Skip to content

Updated android chat example to consume SDK #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 158 additions & 77 deletions mobileChatExamples/androidChatExample/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# Android Native Chat Demo 📱

A native android example app for building custom Amazon Connect Chat. This solution implements basic [ChatWidget](https://docs.aws.amazon.com/connect/latest/adminguide/add-chat-to-website.html) functionality and is capable of Interactive Messages.
This is an example app on how to utilise [AmazonConnectChatAndroid](https://github.com/amazon-connect/amazon-connect-chat-android) SDK


> Refer to [#Specifications](#speficications) for details on compatibility, supported versions, and platforms.

**Reference:**

- Documentation: https://docs.aws.amazon.com/connect/latest/adminguide/enable-chat-in-app.html

https://github.com/amazon-connect/amazon-connect-chat-ui-examples/assets/143978428/1298b153-f476-48d8-aa36-605f3642103a


https://github.com/user-attachments/assets/216d9df8-63ad-473f-a14f-9bc7c5ed3ec3



## Contents

- [Prerequisites](#prerequisites)
- [Local Development](#local-development)
- [How is it working?](#how-is-it-working)
- [Implmentation](#implementation)


## Prerequisites
Expand Down Expand Up @@ -51,83 +56,159 @@ https://github.com/amazon-connect/amazon-connect-chat-ui-examples/assets/1439784

5. Once everything looks okay, Run the app by clicking on ▶️ button `Control + R`or`^ + R`.

## How is it working?

### ChatViewModel
It is responsible for managing the chat state, including initiating the chat, sending and receiving messages, and closing the chat connection when done.

- **Managing Messages**: It holds an array of Message objects, which are published to the UI to reflect real-time chat updates.
- **Handling WebSockets**: ChatViewModel integrates with WebsocketManager to manage WebSocket connections for real-time message delivery.

#### Initialization:
Upon instantiation, ChatViewModel sets up necessary configurations and prepares the AWS Connect Participant client.
- **Chat Initiation**:
When initiateChat is called, it ensures that a WebSocket URL is available, then creates a WebSocket manager instance that listens for incoming messages and events.
- **Message Handling**:
onMessageReceived processes incoming messages and updates the UI accordingly. It filters out typing indicators and handles message status updates (e.g., delivered, read).
- **Chat API Calls**:
The view model interacts with ChatRepository to API calls to start chat sessions (startChatContact), create participant connections (createParticipantConnection), and send messages or events (sendChatMessage, sendEvent).

### ChatRepository
- **Interfacing with AWS Services**: Makes HTTP calls and Utilizes the AWS Connect Participant Service to register a participant and establish a chat session.

### WebsocketManager:
The WebsocketManager handles the WebSocket connection lifecycle and receives chat messages and other events.

- **WebSocket Connection**:
Manages the WebSocket connection, handling connect and disconnect events, and transmitting chat messages.
- **Receiving Messages**:
Implements the didReceive method to handle different WebSocket events, such as incoming text messages that are then passed to the messageCallback.
- **Connection**:
Connects to the WebSocket using the provided URL and listens for events.
- **Event Handling**:
On receiving events, it delegates processing to the appropriate handlers, for instance:
Messages are processed and passed to ChatViewModel through the messageCallback.
Connection status changes are logged, and isConnected status is updated.
- **Message Distribution**:
Incoming text messages are deserialized and depending on their type (MESSAGE, EVENT, etc.), appropriate actions are taken, such as updating UI or acknowledging message receipt.
- **websocketDidConnect**: Called when the WebSocket connects, and may subscribe to topics if necessary.
- **websocketDidReceiveMessage**: Parses and handles incoming messages, delegating them back to ChatManager for UI updates.


### Chat Rehydration

Chat rehydration is a feature that allows users to continue their previous chat sessions. This process involves several checks and actions:

- **Check for Participant Token:**
On initiating chat, the module first checks if a `participantToken` exists.
- If it exists, the module proceeds to fetch the chat transcript, allowing the user to continue from where they left off.
- If it does not exist, the module then checks for a `contactId`.

- **Use of Contact ID:**
If a `contactId` exists, the module prompts the user to either restore the previous session or start a new chat.
- If the user chooses to restore, the module starts a new chat session with the existing `contactId`, creates a new participant connection, and then fetches the transcript.
- If the user opts for a new chat, the module deletes the stored `contactId` and `participantToken` from the storage, ensuring a fresh start. The chat begins with no prior context, emulating the start of a new conversation.

> There will be a new `initialContactId` when chat is rehydrated. The existing `contactId` will only be used as the `sourceContactId`. You may need to use the new `initialContactId` for the `CreateParticipantConnection` call or other APIs if you want to operate on the new contact.

- **Deleting Stored Values:**
For users who opt to start a new chat, the module ensures that previous session identifiers are cleared. This action prevents any overlap or confusion between different chat sessions. By removing the `participantToken` and `contactId`, the ChatViewModel guarantees that the new chat session does not carry over any data or context from previous sessions.

```mermaid
flowchart TD
style D fill:#fc6b03 size:10
A[Start Chat] --> B{Check for participantToken}
B -->|Exists| J[Fetch Chat Transcript]
B -->|Does not exist| D{Check for contactId}
D -->|Exists| E{User Choice}
E -->|Restore| F[Use existing contactId]
E -->|New Chat| G[Delete stored contactId and participantToken]
F --> H[Create new participant connection]
G --> I[Start fresh chat session]
H --> J[Fetch Chat Transcript]
I --> J
J --> K[Continue where left off]
## Implementation

The first step is to call the `StartChatContact` API and pass the response details into the SDK’s `ChatSession` object. Here are some examples of how we would set this up in Kotlin. For reference, you can visit the `androidChatExample` demo within the Amazon Connect Chat UI Examples GitHub repository.

### Configuring and Using `ChatSession` in Your Project

The first step to leveraging the Amazon Connect Chat SDK after installation is to import the library into your file. Next, let's call the StartChatContact API and pass the response details into the SDK’s ChatSession object. Here is an [example](TODO - Add link to UI Example) of how we would set this up in Kotlin. For reference, you can visit the [AndroidChatExample demo](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master/mobileChatExamples/androidChatExample) within the [Amazon Connect Chat UI Examples](https://github.com/amazon-connect/amazon-connect-chat-ui-examples/tree/master) GitHub repository.

The majority of the SDKs functionality will be accessed through the `ChatSession` object. In order to use this object in the file, you can inject it using `@HiltViewModel`:

```
class ChatViewModel @Inject constructor(
private val chatSession: ChatSession, // Injected ChatSession
private val chatRepository: ChatRepository,
private val sharedPreferences: SharedPreferences,
) : ViewModel() {
```

If you are not using Hilt, then you can initialise `ChatSession` like this:

```
private val chatSession = ChatSessionProvider.getChatSession(context)
```

In this example, we are using a `ChatViewModel` class that helps bridge UI and SDK communication. This class is responsible for managing interactions with the SDK's ChatSession object. From here, we can access the SDK's suite of APIs from the `chatSession` property.

Before using the chatSession object, we need to set the config for it via the GlobalConfig object. Most importantly, the GlobalConfig object will be used to set the AWS region that your Connect instance lives in. Here is an example of how to configure the ChatSession object:

```
private suspend fun configureChatSession() {
val globalConfig = GlobalConfig(region = chatConfiguration.region)
chatSession.configure(globalConfig)
...
}
```

From here, you are now ready to interact with the chat via the `ChatSession` object.

#### SendMessage
```kotlin
fun sendMessage(text: String) {
viewModelScope.launch {
if (text.isNotEmpty()) {
val result = chatSession.sendMessage(ContentType.RICH_TEXT, text)
result.onSuccess {
// Handle success - update UI or state as needed
}.onFailure { exception ->
// Handle failure - update UI or state, log error, etc.
Log.e("ChatViewModel", "Error sending message: ${exception.message}")
}
}
}
}
```

Sample demo:

https://github.com/amazon-connect/amazon-connect-chat-ui-examples/assets/143978428/ae078271-2699-4bae-b04a-503a3ac1bfdd
### How to receive messages
```
chatSession.onMessageReceived = { transcriptItem ->
// Handle received websocket message if needed
}

chatSession.onTranscriptUpdated = { transcriptList ->
Log.d("ChatViewModel", "Transcript onTranscriptUpdated last 3 items: ${transcriptList.takeLast(3)}")
viewModelScope.launch {
onUpdateTranscript(transcriptList)
}
}
```

#### SendEvent
```kotlin
fun sendEvent(content: String = "", contentType: ContentType) {
viewModelScope.launch {
val result = chatSession.sendEvent(contentType, content)
result.onSuccess {
// Handle success - update UI or state as needed
}.onFailure { exception ->
// Handle failure - update UI or state, log error, etc.
Log.e("ChatViewModel", "Error sending event: ${exception.message}")
}
}
}
```
#### GetTranscript
```kotlin
fun fetchTranscript(onCompletion: (Boolean) -> Unit) {
viewModelScope.launch {
chatSession.getTranscript(ScanDirection.BACKWARD, SortKey.DESCENDING, 30, null, messages?.get(0)?.id).onSuccess {
Log.d("ChatViewModel", "Transcript fetched successfully")
onCompletion(true)
}.onFailure {
Log.e("ChatViewModel", "Error fetching transcript: ${it.message}")
onCompletion(false)
}
}
}
```
#### Disconnect
```kotlin
fun endChat() {
clearParticipantToken()
viewModelScope.launch {
chatSession.disconnect() // Disconnect from chat session
}
}
```

### Setting Up Chat Event Handlers
The ChatSession object also exposes handlers for common chat events for users to build on. Here is an example code block that demonstrates how you can register event handlers to chat events.

```kotlin
private suspend fun setupChatHandlers(chatSession: ChatSession) {
chatSession.onConnectionEstablished = {
Log.d("ChatViewModel", "Connection established.")
_isChatActive.value = true
}

chatSession.onMessageReceived = { transcriptItem ->
// Handle received websocket message if needed
}

chatSession.onTranscriptUpdated = { transcriptList ->
Log.d("ChatViewModel", "Transcript onTranscriptUpdated last 3 items: ${transcriptList.takeLast(3)}")
viewModelScope.launch {
onUpdateTranscript(transcriptList)
}
}

chatSession.onChatEnded = {
Log.d("ChatViewModel", "Chat ended.")
_isChatActive.value = false
}

chatSession.onConnectionBroken = {
Log.d("ChatViewModel", "Connection broken.")
}

chatSession.onConnectionReEstablished = {
Log.d("ChatViewModel", "Connection re-established.")
_isChatActive.value = true
}

chatSession.onChatSessionStateChanged = {
Log.d("ChatViewModel", "Chat session state changed: $it")
_isChatActive.value = it
}

chatSession.onDeepHeartBeatFailure = {
Log.d("ChatViewModel", "Deep heartbeat failure")
}
}
```

## Specifications

Expand Down
16 changes: 15 additions & 1 deletion mobileChatExamples/androidChatExample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
/build
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
53 changes: 31 additions & 22 deletions mobileChatExamples/androidChatExample/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ plugins {
}

android {
namespace = "com.blitz.androidchatexample"
namespace = "com.amazon.connect.chat.androidchatexample"
compileSdk = 34

defaultConfig {
applicationId = "com.blitz.androidchatexample"
applicationId = "com.amazon.connect.chat.androidchatexample"
minSdk = 24
targetSdk = 34
versionCode = 1
Expand Down Expand Up @@ -58,43 +58,44 @@ android {

dependencies {

implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.0")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("androidx.activity:activity-compose:1.9.3")
implementation(platform("androidx.compose:compose-bom:2024.10.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.2")
implementation("com.google.android.gms:play-services-basement:18.2.0")
implementation("androidx.compose.material3:material3:1.3.0")
implementation("androidx.compose.material:material-icons-extended:1.7.0")
implementation("com.google.android.gms:play-services-basement:18.4.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.10.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

//lifecycle livedata
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.6")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

//Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
implementation("com.squareup:otto:1.3.8")
implementation("com.squareup.retrofit2:adapter-rxjava2:2.3.0")

//Hilt
implementation("com.google.dagger:hilt-android:2.48.1")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
kapt("androidx.hilt:hilt-compiler:1.1.0")
implementation("androidx.navigation:navigation-compose:2.7.5")
kapt("com.google.dagger:hilt-android-compiler:2.48.1")
implementation("com.google.dagger:hilt-android:2.49")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
kapt("androidx.hilt:hilt-compiler:1.2.0")
implementation("androidx.navigation:navigation-compose:2.8.3")
kapt("com.google.dagger:hilt-android-compiler:2.49")

//AWS : https://github.com/aws-amplify/aws-sdk-android
implementation("com.amazonaws:aws-android-sdk-core:2.73.0")
Expand All @@ -106,11 +107,19 @@ dependencies {
// Image loading
implementation("io.coil-kt:coil-compose:2.5.0")

// Pull to refresh
implementation("com.google.accompanist:accompanist-swiperefresh:0.32.0")

//Amazon Connect Chat SDK https://github.com/amazon-connect/amazon-connect-chat-android/
implementation("software.aws.connect:amazon-connect-chat-android:0.0.1-alpha")


}

ruler {
abi.set("arm64-v8a")
locale.set("en")
screenDensity.set(480)
sdkVersion.set(33)
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.blitz.androidchatexample
package com.amazon.connect.chat.androidchatexample

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
Expand All @@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.blitz.androidchatexample", appContext.packageName)
assertEquals("com.amazon.connect.chat.androidchatexample", appContext.packageName)
}
}
Loading