IVS Chat Client Messaging SDK: Android Tutorial Part 1: Chat Rooms
This is the first of a two-part tutorial. You will learn the essentials of working with
the Amazon IVS Chat Messaging SDK by building a fully functional Android app using the
Kotlin
Before you start the module, take a few minutes to familiarize yourself with the prerequisites, key concepts behind chat tokens, and the backend server necessary for creating chat rooms.
These tutorials are created for experienced Android developers who are new to the IVS Chat Messaging SDK. You will need to be comfortable with the Kotlin programming language and creating UIs on the Android platform.
This first part of the tutorial is broken up into several sections:
For full SDK documentation, start with Amazon IVS Chat Client Messaging SDK
(here in the Amazon IVS Chat User Guide) and the Chat Client
Messaging: SDK for Android Reference
Prerequisites
-
Be familiar with Kotlin and creating applications on the Android platform. If you are unfamiliar with creating applications for Android, learn the basics in the Build your first app
guide for Android developers. -
Thoroughly read and understand Getting Started with Amazon IVS Chat.
-
Create an AWS IAM user with the
CreateChatToken
andCreateRoom
capabilities defined in an existing IAM policy. (See Getting Started with Amazon IVS Chat.) -
Ensure that the secret/access keys for this user are stored in an AWS credentials file. For instructions, see the AWS CLI User Guide (especially Configuration and credential file settings).
-
Create a chat room and save its ARN. See Getting Started with Amazon IVS Chat. (If you don’t save the ARN, you can look it up later with the console or Chat API.)
Set Up a Local Authentication/Authorization Server
Your backend server will be responsible for both creating chat rooms and generating the chat tokens needed for the IVS Chat Android SDK to authenticate and authorize your clients for your chat rooms.
See Create a Chat Token in Getting Started with Amazon IVS Chat. As shown in the flowchart there, your server-side code is responsible for creating a chat token. This means your app must provide its own means of generating a chat token by requesting one from your server-side application.
We use the Ktor
At this point, we expect you have your AWS credentials set up correctly. For step-by-step instructions, see Set up AWS Credentials and Region for Development.
Create a new directory and call it chatterbox
and inside it, another one,
called auth-server
.
Our server folder will have the following structure:
- auth-server - src - main - kotlin - com - chatterbox - authserver - Application.kt - resources - application.conf - logback.xml - build.gradle.kts
Note: you can directly copy/paste the code here into the referenced files.
Next, we add all the necessary dependencies and plugins for our auth server to work:
Kotlin Script:
// ./auth-server/build.gradle.kts plugins { application kotlin("jvm") kotlin("plugin.serialization").version("1.7.10") } application { mainClass.set("io.ktor.server.netty.EngineMain") } dependencies { implementation("software.amazon.awssdk:ivschat:2.18.1") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20") implementation("io.ktor:ktor-server-core:2.1.3") implementation("io.ktor:ktor-server-netty:2.1.3") implementation("io.ktor:ktor-server-content-negotiation:2.1.3") implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3") implementation("ch.qos.logback:logback-classic:1.4.4") }
Now we need to set up logging functionality for the auth server. (For more
information, see Configure logger
XML:
// ./auth-server/src/main/resources/logback.xml <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="trace"> <appender-ref ref="STDOUT"/> </root> <logger name="org.eclipse.jetty" level="INFO"/> <logger name="io.netty" level="INFO"/> </configuration>
The Ktorapplication.*
file in the
resources
directory, so we add that as well. (For more information, see
Configuration in a file
HOCON:
// ./auth-server/src/main/resources/application.conf ktor { deployment { port = 3000 } application { modules = [ com.chatterbox.authserver.ApplicationKt.main ] } }
Finally, let's implement our server:
Kotlin:
// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt package com.chatterbox.authserver import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import software.amazon.awssdk.services.ivschat.IvschatClient import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest @Serializable data class ChatTokenParams(var userId: String, var roomIdentifier: String) @Serializable data class ChatToken( val token: String, val sessionExpirationTime: String, val tokenExpirationTime: String, ) fun Application.main() { install(ContentNegotiation) { json(Json) } routing { post("/create_chat_token") { val callParameters = call.receive<ChatTokenParams>() val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier) .userId(callParameters.userId).build() val token = IvschatClient.create() .createChatToken(request) call.respond( ChatToken( token.token(), token.sessionExpirationTime().toString(), token.tokenExpirationTime().toString() ) ) } } }
Create a Chatterbox Project
To create an Android project, install and open Android Studio
Follow the steps listed in the official Android Create a Project
guide
-
In Choose your project type
, choose the Empty Activity project template for our Chatterbox app. -
In Configure your project
, choose the following values for configuration fields: -
Name: My App
-
Package name: com.chatterbox.myapp
-
Save location: point at the created
chatterbox
directory created in the previous step -
Language: Kotlin
-
Minimum API level: API 21: Android 5.0 (Lollipop)
-
After specifying all the configuration parameters correctly, our file structure inside
the chatterbox
folder should look like the following:
- app - build.gradle ... - gradle - .gitignore - build.gradle - gradle.properties - gradlew - gradlew.bat - local.properties - settings.gradle - auth-server - src - main - kotlin - com - chatterbox - authserver - Application.kt - resources - application.conf - logback.xml - build.gradle.kts
Now that we have a working Android project, we can add com.amazonaws:ivs-chat-messagingbuild.gradle
dependencies. (For more information on the Gradle
Note: At the top of every code snippet, there is a path to the file where you should be making changes in your project. The path is relative to the project’s root.
In the code below, replace <version>
with the
current version number of the Chat Android SDK (e.g., 1.0.0).
Kotlin:
// ./app/build.gradle plugins { // ... } android { // ... } dependencies { implementation("com.amazonaws:ivs-chat-messaging:<version>") // ... }
After the new dependency is added, run Sync Project with Gradle
Files in Android Studio to synchronize the project with the new
dependency. (For more information, see Add build
dependencies
To conveniently run our auth server (created in the previous section) from the project
root, we include it as a new module in settings.gradle
. (For more
information, see Structuring and Building a Software Component with Gradle
Kotlin Script:
// ./settings.gradle // ... rootProject.name = "Chatterbox" include ':app' include ':auth-server'
From now on, as auth-server
is included in the Android project, you can
run the auth server with the following command from the project's root:
Shell:
./gradlew :auth-server:run
Connect to a Chat Room and Observe Connection Updates
To open a chat-room connection, we use the onCreate() activity lifecycle callbackregion
and
tokenProvider
to instantiate a room connection.
Note: The fetchChatToken
function in the
snippet below will be implemented in the next section.
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... import androidx.appcompat.app.AppCompatActivity // ... // AWS region of the room that was created in Getting Started with Amazon IVS Chat const val REGION = "us-west-2" class MainActivity : AppCompatActivity() { private var room: ChatRoom? = null // ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Create room instance room = ChatRoom(REGION, ::fetchChatToken) } // ... }
Displaying and reacting to changes in a chat room's connection are essential parts of
making a chat app like chatterbox
. Before we can start interacting with the
room, we must subscribe to chat-room connection-state events, to get updates.
ChatRoom
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt // ... package com.chatterbox.myapp // ... const val TAG = "IVSChat-App" class MainActivity : AppCompatActivity() { // ... private val roomListener = object : ChatRoomListener { override fun onConnecting(room: ChatRoom) { Log.d(TAG, "onConnecting") } override fun onConnected(room: ChatRoom) { Log.d(TAG, "onConnected") } override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) { Log.d(TAG, "onDisconnected $reason") } override fun onMessageReceived(room: ChatRoom, message: ChatMessage) { Log.d(TAG, "onMessageReceived $message") } override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) { Log.d(TAG, "onMessageDeleted $event") } override fun onEventReceived(room: ChatRoom, event: ChatEvent) { Log.d(TAG, "onEventReceived $event") } override fun onUserDisconnected(room: ChatRoom, event: DisconnectUserEvent) { Log.d(TAG, "onUserDisconnected $event") } } }
Now that we have ChatRoomListener
implemented, we attach it to our room
instance:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Create room instance room = ChatRoom(REGION, ::fetchChatToken).apply { listener = roomListener } } private val roomListener = object : ChatRoomListener { // ... }
After this, we need to provide the ability to read the room-connection state. We will
keep it in the MainActivity.kt
propertyChatRoom state
in the
IVS
Chat Android SDK ReferenceupdateConnectionState
:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... enum class ConnectionState { CONNECTED, DISCONNECTED, LOADING } class MainActivity : AppCompatActivity() { private var connectionState = ConnectionState.DISCONNECTED // ... private fun updateConnectionState(state: ConnectionState) { connectionState = state when (state) { ConnectionState.CONNECTED -> { Log.d(TAG, "room connected") } ConnectionState.DISCONNECTED -> { Log.d(TAG, "room disconnected") } ConnectionState.LOADING -> { Log.d(TAG, "room loading") } } } }
Next, we integrate our state-updater function with the ChatRoom.listener
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... private val roomListener = object : ChatRoomListener { override fun onConnecting(room: ChatRoom) { Log.d(TAG, "onConnecting") runOnUiThread { updateConnectionState(ConnectionState.LOADING) } } override fun onConnected(room: ChatRoom) { Log.d(TAG, "onConnected") runOnUiThread { updateConnectionState(ConnectionState.CONNECTED) } } override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) { Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected") runOnUiThread { updateConnectionState(ConnectionState.DISCONNECTED) } } } }
Now that we have the ability to save, listen, and react to ChatRoom
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... enum class ConnectionState { CONNECTED, DISCONNECTED, LOADING } class MainActivity : AppCompatActivity() { private var connectionState = ConnectionState.DISCONNECTED // ... private fun connect() { try { room?.connect() } catch (ex: Exception) { Log.e(TAG, "Error while calling connect()", ex) } } private val roomListener = object : ChatRoomListener { // ... override fun onConnecting(room: ChatRoom) { Log.d(TAG, "onConnecting") runOnUiThread { updateConnectionState(ConnectionState.LOADING) } } override fun onConnected(room: ChatRoom) { Log.d(TAG, "onConnected") runOnUiThread { updateConnectionState(ConnectionState.CONNECTED) } } // ... } }
Build a Token Provider
It's time to create a function responsible for creating and managing chat tokens in
our application. In this example we use the Retrofit HTTP client for
Android
Before we can send any network traffic, we must set up a network-security
configuration for Android. (For more information, see Network
security configurationuser-permission
tag and
networkSecurityConfig
attribute, which will point to our new
network-security configuration. In the code below, replace
<version>
with the current version number of the Chat Android
SDK (e.g., 1.0.0).
XML:
// ./app/src/main/AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.chatterbox.myapp"> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:fullBackupContent="@xml/backup_rules" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" // ... // ./app/build.gradle dependencies { implementation("com.amazonaws:ivs-chat-messaging:<version>") // ... implementation("com.squareup.retrofit2:retrofit:2.9.0") }
Declare 10.0.2.2
and localhost
domains as trusted, to start
exchanging messages with our backend:
XML:
// ./app/src/main/res/xml/network_security_config.xml <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">10.0.2.2</domain> <domain includeSubdomains="true">localhost</domain> </domain-config> </network-security-config>
Next, we need to add a new dependency, along with Gson
converter addition<version>
with the current version number of
the Chat Android SDK (e.g., 1.0.0).
Kotlin Script:
// ./app/build.gradle dependencies { implementation("com.amazonaws:ivs-chat-messaging:<version>") // ... implementation("com.squareup.retrofit2:retrofit:2.9.0") }
To retrieve a chat token, we need to make a POST HTTP request from our
chatterbox
app. We define the request in an interface for Retrofit to
implement. (See Retrofit
documentation
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt package com.chatterbox.myapp.network // ... import androidx.annotation.Keep import com.amazonaws.ivs.chat.messaging.ChatToken import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST data class CreateTokenParams(var userId: String, var roomIdentifier: String) interface ApiService { @POST("create_chat_token") fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken> }
Now, with networking set up, it's time to add a function responsible for creating and
managing our chat token. We add it to MainActivity.kt
, which was
automatically created when the project was generated:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import com.amazonaws.ivs.chat.messaging.* import com.chatterbox.myapp.network.CreateTokenParams import com.chatterbox.myapp.network.RetrofitFactory import retrofit2.Call import java.io.IOException import retrofit2.Callback import retrofit2.Response // custom tag for logging purposes const val TAG = "IVSChat-App" // any ID to be associated with auth token const val USER_ID = "test user id" // ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs) const val ROOM_ID = "arn:aws:..." // AWS region of the room that was created in Getting Started with Amazon IVS Chat const val REGION = "us-west-2" class MainActivity : AppCompatActivity() { private val service = RetrofitFactory.makeRetrofitService() private lateinit var userId: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } private fun fetchChatToken(callback: ChatTokenCallback) { val params = CreateTokenParams(userId, ROOM_ID) service.createChatToken(params).enqueue(object : Callback<ChatToken> { override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) { val token = response.body() if (token == null) { Log.e(TAG, "Received empty token response") callback.onFailure(IOException("Empty token response")) return } Log.d(TAG, "Received token response $token") callback.onSuccess(token) } override fun onFailure(call: Call<ChatToken>, throwable: Throwable) { Log.e(TAG, "Failed to fetch token", throwable) callback.onFailure(throwable) } }) } }
Next Steps
Now that you’ve established a chat-room connection, proceed to Part 2 of this Android tutorial, Messages and Events.