From 1963d4b8e9d9fd9a02380c36383b9cec31c8e7d8 Mon Sep 17 00:00:00 2001 From: Michael Liao Date: Fri, 11 Apr 2025 12:45:13 -0700 Subject: [PATCH 1/3] Fixing getTranscript loop on reconnection flow --- .../chat/sdk/repository/ChatService.kt | 23 ++++++-- .../sdk/repository/ChatServiceImplTest.kt | 57 ++++++++++++++++++- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt index 6239e1a..2e389cd 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt @@ -259,7 +259,7 @@ class ChatServiceImpl @Inject constructor( SDKLogger.logger.logDebug { "Connection Re-Established" } connectionDetailsProvider.setChatSessionState(true) removeTypingIndicators() // Make sure to remove typing indicators if still present - fetchReconnectedTranscript(internalTranscript) + fetchReconnectedTranscript() } ChatEvent.ChatEnded -> { @@ -715,7 +715,7 @@ class ChatServiceImpl @Inject constructor( } } - private suspend fun fetchReconnectedTranscript(internalTranscript: List) { + private suspend fun fetchReconnectedTranscript() { val lastItem = internalTranscript.lastOrNull { (it as? Message)?.metadata?.status != MessageStatus.Failed } ?: return @@ -728,19 +728,32 @@ class ChatServiceImpl @Inject constructor( fetchTranscriptWith(startPosition) } + private fun isItemInInternalTranscript(Id: String?): Boolean { + if (Id == null) return false + for (item in internalTranscript.reversed()) { + if (item.id == Id) { + return true + } + } + return false + } + private suspend fun fetchTranscriptWith(startPosition: StartPosition?) { getTranscript(startPosition = startPosition, scanDirection = ScanDirection.FORWARD, sortKey = SortKey.ASCENDING, - maxResults = 30, + maxResults = 100, nextToken = null).onSuccess { transcriptResponse -> if (transcriptResponse.nextToken?.isNotEmpty() == true) { - val newStartPosition = transcriptResponse.transcript.lastOrNull()?.let { + val lastItem = transcriptResponse.transcript.lastOrNull() + val newStartPosition = lastItem?.let { StartPosition().apply { id = it.id } } - fetchTranscriptWith(startPosition = newStartPosition) + if (!isItemInInternalTranscript(lastItem?.id)) { + fetchTranscriptWith(startPosition = newStartPosition) + } } }.onFailure { error -> SDKLogger.logger.logError { "Error fetching transcript with startPosition $startPosition: ${error.localizedMessage}" } diff --git a/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt b/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt index b9a1031..2aba750 100644 --- a/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt +++ b/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt @@ -18,6 +18,7 @@ import com.amazon.connect.chat.sdk.provider.ConnectionDetailsProvider import com.amazonaws.regions.Regions import com.amazonaws.services.connectparticipant.model.DisconnectParticipantResult import com.amazonaws.services.connectparticipant.model.GetTranscriptResult +import com.amazonaws.services.connectparticipant.model.Item import com.amazonaws.services.connectparticipant.model.ScanDirection import com.amazonaws.services.connectparticipant.model.SendEventResult import com.amazonaws.services.connectparticipant.model.SendMessageResult @@ -48,6 +49,12 @@ import org.robolectric.RobolectricTestRunner import io.mockk.every import io.mockk.mockkStatic import io.mockk.unmockkStatic +import junit.framework.TestCase.fail +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.setMain +import org.mockito.kotlin.any import java.util.UUID import java.net.URL @@ -76,7 +83,7 @@ class ChatServiceImplTest { @Mock private lateinit var messageReceiptsManager: MessageReceiptsManager - private lateinit var chatService: ChatService + private lateinit var chatService: ChatServiceImpl private lateinit var eventSharedFlow: MutableSharedFlow private lateinit var transcriptSharedFlow: MutableSharedFlow private lateinit var chatSessionStateFlow: MutableStateFlow @@ -84,10 +91,12 @@ class ChatServiceImplTest { private lateinit var newWsUrlFlow: MutableSharedFlow private val mockUri: Uri = Uri.parse("https://example.com/dummy.pdf") + private val testDispatcher = StandardTestDispatcher() @Before fun setUp() { MockitoAnnotations.openMocks(this) + Dispatchers.setMain(testDispatcher) eventSharedFlow = MutableSharedFlow() transcriptSharedFlow = MutableSharedFlow() @@ -510,6 +519,48 @@ class ChatServiceImplTest { verify(awsClient).getTranscript(anyOrNull()) } + private fun createMockItem(id: String, timestamp: String): Item { + val item = Item() + item.absoluteTime = timestamp + item.content = "test${id}" + item.contentType = "text/plain" + item.id = id + item.type = "MESSAGE" + item.participantId = id + item.displayName = "test${id}" + item.participantRole = "CUSTOMER" + return item + } + + @Test + fun test_fetchReconnectedTranscript_success() = runTest { + val chatDetails = ChatDetails(participantToken = "token") + val mockConnectionDetails = createMockConnectionDetails("valid_token") + `when`(connectionDetailsProvider.getConnectionDetails()).thenReturn(mockConnectionDetails) + chatService.createChatSession(chatDetails) + advanceUntilIdle() + + // Create a mock GetTranscriptResult and configure it to return expected values + val mockGetTranscriptResult = mock() + + `when`(mockGetTranscriptResult.initialContactId).thenReturn("") + `when`(awsClient.getTranscript(anyOrNull())).thenReturn(Result.success(mockGetTranscriptResult)) + + // Add items to the internal transcript and emit reconnection event. + // This scenario should call getTranscript twice since nextToken is defined in the first call. + `when`(mockGetTranscriptResult.transcript).thenReturn(listOf()) + `when`(mockGetTranscriptResult.nextToken).thenReturn("nextToken1").thenReturn("") + + val transcriptItem1 = Message(id = "1", timeStamp = "2024-01-01T00:00:00Z", participant = "user", contentType = "text/plain", text = "Hello") + val transcriptItem2 = Message(id = "2", timeStamp = "2025-01-01T00:01:00Z", participant = "agent", contentType = "text/plain", text = "Hi") + chatService.internalTranscript.add(transcriptItem1) + chatService.internalTranscript.add(transcriptItem2) + val chatEvent = ChatEvent.ConnectionReEstablished + eventSharedFlow.emit(chatEvent) + advanceUntilIdle() + verify(awsClient, times(2)).getTranscript(anyOrNull()) + } + @Test fun test_sendMessageReceipt_success() = runTest { val messageId = "messageId123" @@ -615,7 +666,7 @@ class ChatServiceImplTest { // Add message in internal transcript val transcriptItem = Message(id = "1", timeStamp = "mockedTimestamp", participant = "user", contentType = "text/plain", text = "Hello") - (chatService as ChatServiceImpl).internalTranscript.add(0, transcriptItem) + chatService.internalTranscript.add(0, transcriptItem) // Execute reset chatService.reset() @@ -623,7 +674,7 @@ class ChatServiceImplTest { // Validate that websocket disconnected, tokens are reset and internal transcript is deleted verify(webSocketManager).disconnect("Resetting ChatService") verify(connectionDetailsProvider).reset() - assertEquals(0, (chatService as ChatServiceImpl).internalTranscript.size) + assertEquals(0, chatService.internalTranscript.size) } private fun createMockConnectionDetails(token : String): ConnectionDetails { From 0a339743667e4151754546e4377483755886fca8 Mon Sep 17 00:00:00 2001 From: Michael Liao Date: Fri, 11 Apr 2025 15:11:24 -0700 Subject: [PATCH 2/3] Updating logic + bumping version to 1.0.10 --- .../amazon/connect/chat/sdk/repository/ChatService.kt | 10 +++------- chat-sdk/version.properties | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt index 2e389cd..3be2230 100644 --- a/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt +++ b/chat-sdk/src/main/java/com/amazon/connect/chat/sdk/repository/ChatService.kt @@ -744,13 +744,9 @@ class ChatServiceImpl @Inject constructor( sortKey = SortKey.ASCENDING, maxResults = 100, nextToken = null).onSuccess { transcriptResponse -> - if (transcriptResponse.nextToken?.isNotEmpty() == true) { - val lastItem = transcriptResponse.transcript.lastOrNull() - val newStartPosition = lastItem?.let { - StartPosition().apply { - id = it.id - } - } + val lastItem = transcriptResponse.transcript.lastOrNull() + if (transcriptResponse.nextToken?.isNotEmpty() == true && lastItem != null) { + val newStartPosition = StartPosition().apply { id = lastItem.id } if (!isItemInInternalTranscript(lastItem?.id)) { fetchTranscriptWith(startPosition = newStartPosition) } diff --git a/chat-sdk/version.properties b/chat-sdk/version.properties index 2976a51..c232642 100644 --- a/chat-sdk/version.properties +++ b/chat-sdk/version.properties @@ -1,3 +1,3 @@ -sdkVersion=1.0.9 +sdkVersion=1.0.10 groupId=software.aws.connect artifactId=amazon-connect-chat-android From cde4e15a05bf66386a832bf538333e4c9c094d86 Mon Sep 17 00:00:00 2001 From: Michael Liao Date: Fri, 11 Apr 2025 15:28:22 -0700 Subject: [PATCH 3/3] test fix --- .../connect/chat/sdk/repository/ChatServiceImplTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt b/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt index 2aba750..83cefa9 100644 --- a/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt +++ b/chat-sdk/src/test/java/com/amazon/connect/chat/sdk/repository/ChatServiceImplTest.kt @@ -547,9 +547,9 @@ class ChatServiceImplTest { `when`(awsClient.getTranscript(anyOrNull())).thenReturn(Result.success(mockGetTranscriptResult)) // Add items to the internal transcript and emit reconnection event. - // This scenario should call getTranscript twice since nextToken is defined in the first call. + // This scenario should call getTranscript once since the empty transcript response. `when`(mockGetTranscriptResult.transcript).thenReturn(listOf()) - `when`(mockGetTranscriptResult.nextToken).thenReturn("nextToken1").thenReturn("") + `when`(mockGetTranscriptResult.nextToken).thenReturn("nextToken1") val transcriptItem1 = Message(id = "1", timeStamp = "2024-01-01T00:00:00Z", participant = "user", contentType = "text/plain", text = "Hello") val transcriptItem2 = Message(id = "2", timeStamp = "2025-01-01T00:01:00Z", participant = "agent", contentType = "text/plain", text = "Hi") @@ -558,7 +558,7 @@ class ChatServiceImplTest { val chatEvent = ChatEvent.ConnectionReEstablished eventSharedFlow.emit(chatEvent) advanceUntilIdle() - verify(awsClient, times(2)).getTranscript(anyOrNull()) + verify(awsClient, times(1)).getTranscript(anyOrNull()) } @Test