Skip to content
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

Bug fixes and added new callbacks #31

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies {

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`:
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(
Expand All @@ -64,6 +64,12 @@ class ChatViewModel @Inject constructor(
) : 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ fun ChatView(viewModel: ChatViewModel, activity: Activity) {
if(!selectedFileName?.lastPathSegment.isNullOrEmpty()) {
selectedFileName?.let { viewModel.uploadAttachment(it) }
}
if (textInput.isNotEmpty()) {
if (textInput.trim().isNotEmpty()) {
viewModel.sendMessage(textInput)
}
textInput = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ object CommonUtils {
MessageStatus.Sending -> "Sending"
MessageStatus.Failed -> "Failed to send"
MessageStatus.Sent -> "Sent"
MessageStatus.Custom -> status.customValue ?: "Custom status"
else -> "" // Returning empty string for unknown or null status
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.amazon.connect.chat.androidchatexample.viewmodel

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
Expand All @@ -19,6 +20,7 @@ import com.amazon.connect.chat.androidchatexample.network.Resource
import com.amazon.connect.chat.androidchatexample.repository.ChatRepository
import com.amazon.connect.chat.androidchatexample.utils.CommonUtils
import com.amazon.connect.chat.sdk.ChatSession
import com.amazon.connect.chat.sdk.ChatSessionProvider
import com.amazon.connect.chat.sdk.model.ChatDetails
import com.amazon.connect.chat.sdk.model.ContentType
import com.amazon.connect.chat.sdk.model.Event
Expand All @@ -29,6 +31,7 @@ import com.amazon.connect.chat.sdk.model.TranscriptItem
import com.amazonaws.services.connectparticipant.model.ScanDirection
import com.amazonaws.services.connectparticipant.model.SortKey
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import java.net.URL
import javax.inject.Inject
Expand All @@ -40,6 +43,10 @@ class ChatViewModel @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val chatConfigProvider: ChatConfigProvider
) : ViewModel() {

// If you are not using Hilt, you can initialize ChatSession like this
// private val chatSession = ChatSessionProvider.getChatSession(context)
mrajatttt marked this conversation as resolved.
Show resolved Hide resolved

private val _isLoading = MutableLiveData(false)
val isLoading: MutableLiveData<Boolean> = _isLoading

Expand Down Expand Up @@ -106,7 +113,7 @@ class ChatViewModel @Inject constructor(
}

chatSession.onTranscriptUpdated = { transcriptList ->
Log.d("ChatViewModel", "Transcript onTranscriptUpdated: $transcriptList")
Log.d("ChatViewModel", "Transcript onTranscriptUpdated last 3 items: ${transcriptList.takeLast(3)}")
viewModelScope.launch {
onUpdateTranscript(transcriptList)
}
Expand All @@ -130,6 +137,10 @@ class ChatViewModel @Inject constructor(
Log.d("ChatViewModel", "Chat session state changed: $it")
_isChatActive.value = it
}

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

fun initiateChat() {
Expand Down Expand Up @@ -217,7 +228,6 @@ class ChatViewModel @Inject constructor(
transcriptItem
}
messages.addAll(updatedMessages)
Log.d("ChatViewModel", "Transcript updated: $messages")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ fun AttachmentMessageView(
Text(
text = CommonUtils.customMessageStatus(message.metadata?.status),
fontSize = 10.sp,
color = Color.Gray
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,28 @@ fun EventView(event: Event) {
contentAlignment = alignment
) {
if (isTypingEvent) {
TypingIndicator()
Column(
modifier = Modifier
.fillMaxWidth()
.background(
Color(0xFFFFFFFF),
RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
event.displayName?.let {
Text(
text = it,
color = Color.Black,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
TypingIndicator()
}
} else if (event.eventDirection == MessageDirection.COMMON) {
event.text?.let {
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ fun TypingIndicator() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(10.dp)
.background(Color(0xFFEDEDED), RoundedCornerShape(8.dp))
.padding( start = 15.dp)
.padding( end = 10.dp)
Expand Down
43 changes: 26 additions & 17 deletions chat-sdk/src/main/java/com/amazon/connect/chat/sdk/ChatSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,11 @@ interface ChatSession {
var onConnectionEstablished: (() -> Unit)?
var onConnectionReEstablished: (() -> Unit)?
var onConnectionBroken: (() -> Unit)?
var onDeepHeartBeatFailure: (() -> Unit)?
var onMessageReceived: ((TranscriptItem) -> Unit)?
var onTranscriptUpdated: ((List<TranscriptItem>) -> Unit)?
var onChatEnded: (() -> Unit)?
var isChatSessionActive: Boolean
}

@Singleton
Expand All @@ -115,17 +117,18 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
override var onConnectionEstablished: (() -> Unit)? = null
override var onConnectionReEstablished: (() -> Unit)? = null
override var onConnectionBroken: (() -> Unit)? = null
override var onDeepHeartBeatFailure: (() -> Unit)? = null
override var onMessageReceived: ((TranscriptItem) -> Unit)? = null
override var onTranscriptUpdated: ((List<TranscriptItem>) -> Unit)? = null
override var onChatEnded: (() -> Unit)? = null
override var onChatSessionStateChanged: ((Boolean) -> Unit)? = null
override var isChatSessionActive: Boolean = false
private val coroutineScope = CoroutineScope(Dispatchers.Main + Job())
private var eventCollectionJob: Job? = null
private var transcriptCollectionJob: Job? = null
private var transcriptListCollectionJob: Job? = null
private var chatSessionStateCollectionJob: Job? = null


private fun setupEventSubscriptions() {
// Cancel any existing subscriptions before setting up new ones
cleanup()
Expand All @@ -136,8 +139,12 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
when (event) {
ChatEvent.ConnectionEstablished -> onConnectionEstablished?.invoke()
ChatEvent.ConnectionReEstablished -> onConnectionReEstablished?.invoke()
ChatEvent.ChatEnded -> onChatEnded?.invoke()
ChatEvent.ChatEnded -> {
onChatEnded?.invoke()
cleanup()
}
ChatEvent.ConnectionBroken -> onConnectionBroken?.invoke()
ChatEvent.DeepHeartBeatFailure -> onDeepHeartBeatFailure?.invoke()
}
}
}
Expand All @@ -164,6 +171,7 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)

chatSessionStateCollectionJob = coroutineScope.launch {
chatService.chatSessionStatePublisher.collect { isActive ->
isChatSessionActive = isActive
onChatSessionStateChanged?.invoke(isActive)
}
}
Expand All @@ -184,8 +192,6 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
override suspend fun disconnect(): Result<Boolean> {
return withContext(Dispatchers.IO) {
chatService.disconnectChatSession()
}.also {
cleanup()
}
}

Expand All @@ -201,18 +207,6 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
}
}

private fun cleanup() {
// Cancel flow collection jobs when disconnecting or cleaning up
eventCollectionJob?.cancel()
transcriptCollectionJob?.cancel()
transcriptListCollectionJob?.cancel()
chatSessionStateCollectionJob?.cancel()

eventCollectionJob = null
transcriptCollectionJob = null
transcriptListCollectionJob = null
chatSessionStateCollectionJob = null
}

override suspend fun sendAttachment(fileUri: Uri): Result<Boolean> {
return withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -274,7 +268,6 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
}
}


private suspend fun sendReceipt(event: MessageReceiptType, messageId: String): Result<Unit> {
return withContext(Dispatchers.IO) {
runCatching {
Expand All @@ -286,4 +279,20 @@ class ChatSessionImpl @Inject constructor(private val chatService: ChatService)
}
}

private fun cleanup() {
// Cancel flow collection jobs when disconnecting or cleaning up
eventCollectionJob?.cancel()
transcriptCollectionJob?.cancel()
transcriptListCollectionJob?.cancel()
chatSessionStateCollectionJob?.cancel()

eventCollectionJob = null
transcriptCollectionJob = null
transcriptListCollectionJob = null
chatSessionStateCollectionJob = null

// Reset active state
isChatSessionActive = false
onChatSessionStateChanged?.invoke(false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.amazon.connect.chat.sdk

import android.content.Context
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent

@EntryPoint
@InstallIn(SingletonComponent::class)
interface ChatSessionEntryPoint {
fun getChatSession(): ChatSession
}

object ChatSessionProvider {
private var chatSession: ChatSession? = null

fun getChatSession(context: Context): ChatSession {
if (chatSession == null) {
val appContext = context.applicationContext
val entryPoint = EntryPointAccessors.fromApplication(
appContext,
ChatSessionEntryPoint::class.java
)
chatSession = entryPoint.getChatSession()
}
return chatSession!!
}
}
16 changes: 0 additions & 16 deletions chat-sdk/src/main/java/com/amazon/connect/chat/sdk/Config.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ enum class ChatEvent {
ConnectionReEstablished,
ChatEnded,
ConnectionBroken,
DeepHeartBeatFailure,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import com.amazonaws.regions.Regions
data class GlobalConfig(
var region: Regions = defaultRegion,
var features: Features = Features.defaultFeatures,
var disableCsm: Boolean = false,
var isDevMode: Boolean = false
var disableCsm: Boolean = false
) {
companion object {
val defaultRegion: Regions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
package com.amazon.connect.chat.sdk.model

enum class MessageStatus(val status: String) {
enum class MessageStatus(val status: String, var customValue: String? = null) {
Delivered("Delivered"),
Read("Read"),
Sending("Sending"),
Failed("Failed"),
Sent("Sent"),
Unknown("Unknown")
Unknown("Unknown"),

Custom("Custom", null);

companion object {
fun custom(message: String): MessageStatus {
return Custom.apply {
Custom.customValue = message
}
}
}

}

interface MessageMetadataProtocol : TranscriptItemProtocol {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.amazon.connect.chat.sdk.network

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.concurrent.timer
import java.util.Timer

class HeartbeatManager(
val sendHeartbeatCallback: () -> Unit,
val missedHeartbeatCallback: () -> Unit
val missedHeartbeatCallback: suspend () -> Unit
) {
private var pendingResponse: Boolean = false
private var timer: Timer? = null

fun startHeartbeat() {
suspend fun startHeartbeat() {
timer?.cancel()
pendingResponse = false
timer = timer(period = 10000) {
Expand All @@ -19,7 +22,9 @@ class HeartbeatManager(
pendingResponse = true
} else {
timer?.cancel()
missedHeartbeatCallback()
CoroutineScope(Dispatchers.IO).launch {
missedHeartbeatCallback()
}
}
}
}
Expand Down
Loading
Loading