From 72399f8a3add9a9d3d5a3e7e9f69fe3f10d849ca Mon Sep 17 00:00:00 2001 From: corinac123 Date: Thu, 2 Jul 2026 15:18:22 +0100 Subject: [PATCH 01/13] Add endpoint to bob * Add an endpoint for Bob * Repository returns all recipients with same name From b43fd56dd9ad47aeb490bc63d53f494c7186c182 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 2 Jul 2026 11:57:14 +0000 Subject: [PATCH 02/13] Removing name as input --- ChatApp/app/build.gradle.kts | 4 ++-- ChatApp/wear/build.gradle.kts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ChatApp/app/build.gradle.kts b/ChatApp/app/build.gradle.kts index 1015a43..0b7f8ef 100644 --- a/ChatApp/app/build.gradle.kts +++ b/ChatApp/app/build.gradle.kts @@ -28,8 +28,8 @@ android { applicationId = "com.example.chatapp" minSdk = 24 targetSdk = 37 - versionCode = 1 - versionName = "1.0" + versionCode = 99999 + versionName = "99999" testInstrumentationRunner = "com.example.chatapp.HiltTestRunner" } diff --git a/ChatApp/wear/build.gradle.kts b/ChatApp/wear/build.gradle.kts index 8c0cd37..1799d71 100644 --- a/ChatApp/wear/build.gradle.kts +++ b/ChatApp/wear/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(libs.androidx.appfunctions.service) ksp(libs.androidx.appfunctions.compiler) + } // AppFunctions ksp option From b7a68675c79ef7321806200dab7d487a081188fb Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 2 Jul 2026 13:56:25 +0000 Subject: [PATCH 03/13] Added deps --- ChatApp/shared/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/ChatApp/shared/build.gradle.kts b/ChatApp/shared/build.gradle.kts index 7d93e0f..4424dbd 100644 --- a/ChatApp/shared/build.gradle.kts +++ b/ChatApp/shared/build.gradle.kts @@ -50,4 +50,5 @@ dependencies { implementation(libs.androidx.appfunctions.service) ksp(libs.androidx.appfunctions.compiler) + } From 54e2c87ed1db9b72d1333b2e041ceb91e478a1c6 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 2 Jul 2026 14:04:48 +0000 Subject: [PATCH 04/13] fix version --- ChatApp/app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ChatApp/app/build.gradle.kts b/ChatApp/app/build.gradle.kts index 0b7f8ef..1015a43 100644 --- a/ChatApp/app/build.gradle.kts +++ b/ChatApp/app/build.gradle.kts @@ -28,8 +28,8 @@ android { applicationId = "com.example.chatapp" minSdk = 24 targetSdk = 37 - versionCode = 99999 - versionName = "99999" + versionCode = 1 + versionName = "1.0" testInstrumentationRunner = "com.example.chatapp.HiltTestRunner" } From 5e089383cac6c45c526acccdd8a1f9fedcbfbd78 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 2 Jul 2026 16:09:52 +0000 Subject: [PATCH 05/13] added search messages --- .../AppFunctionInstrumentationTest.kt | 17 ++- .../chatapp/appfunctions/AppFunctionsTest.kt | 93 +++++++++++---- .../chatapp/data/RecipientsRepository.kt | 8 +- .../chatapp/appfunctions/AppFunctions.kt | 111 ++++++++++++++---- .../chatapp/data/RecipientsRepository.kt | 8 +- 5 files changed, 185 insertions(+), 52 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt index b88194f..3ba0d73 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt @@ -72,12 +72,12 @@ class AppFunctionInstrumentationTest { ) .first() .flatMap { it.appFunctions } - .single { it.id == AppFunctionsIds.SEND_ID } + .single { it.id == AppFunctionsIds.SEND_MESSAGE_ID } val testRecipient = recipientsRepository.getAllRecipients().first() val request = ExecuteAppFunctionRequest( targetPackageName = context.packageName, - AppFunctionsIds.SEND_ID, + AppFunctionsIds.SEND_MESSAGE_ID, AppFunctionData.Builder( sendMessageFunctionMetadata.parameters, sendMessageFunctionMetadata.components, @@ -111,12 +111,12 @@ class AppFunctionInstrumentationTest { ) .first() .flatMap { it.appFunctions } - .single { it.id == AppFunctionsIds.SEND_ID } + .single { it.id == AppFunctionsIds.SEND_MESSAGE_ID } val testRecipient = recipientsRepository.getAllRecipients().first() val request = ExecuteAppFunctionRequest( targetPackageName = context.packageName, - AppFunctionsIds.SEND_ID, + AppFunctionsIds.SEND_MESSAGE_ID, AppFunctionData.Builder( sendMessageFunctionMetadata.parameters, sendMessageFunctionMetadata.components, @@ -153,7 +153,7 @@ class AppFunctionInstrumentationTest { getRecipientsFunctionMetadata.components, ) .setString("query", "Alice") - .setString("filterType", "INDIVIDUAL") + .setString("contactType", "INDIVIDUAL") .build(), ) @@ -167,7 +167,12 @@ class AppFunctionInstrumentationTest { ?.map { it.deserialize(AppFunctions.ContactSearchResult::class.java) }, ) .containsExactly( - AppFunctions.ContactSearchResult(endpointValue = "1", endpointType = "INDIVIDUAL", displayName = "Alice Smith"), + AppFunctions.ContactSearchResult( + endpointValue = "1", + endpointType = "INDIVIDUAL", + contactDisplayName = "Alice Smith", + endpointDisplayName = "alice@example.com", + ), ) } } diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index d4095e3..22b6ced 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -99,7 +99,8 @@ class AppFunctionsTest { runBlocking { val contacts = appFunctions.searchContacts(testContext, "Alice", "INDIVIDUAL") Assert.assertEquals(1, contacts.size) - Assert.assertEquals("Alice Smith", contacts[0].displayName) + Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) + Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) } } @@ -108,7 +109,8 @@ class AppFunctionsTest { runBlocking { val contacts = appFunctions.searchContacts(testContext, "Work", "GROUP") Assert.assertEquals(1, contacts.size) - Assert.assertEquals("Work Friends", contacts[0].displayName) + Assert.assertEquals("Work Friends", contacts[0].contactDisplayName) + Assert.assertEquals("Work Friends", contacts[0].endpointDisplayName) } } @@ -123,20 +125,19 @@ class AppFunctionsTest { @Test fun searchContacts_anyType_returnsMatches() { runBlocking { - // "searchAny" branch (when filterType is not INDIVIDUAL or GROUP) val contacts = appFunctions.searchContacts(testContext, "Alice", "ANY") Assert.assertEquals(1, contacts.size) - Assert.assertEquals("Alice Smith", contacts[0].displayName) + Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) + Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) } } @Test - fun send_validMessage_returnsSuccess() { + fun sendMessage_validMessage_returnsSuccess() { runTest { - val result = appFunctions.send(testContext, "1", "Hello") + val result = appFunctions.sendMessage(testContext, "1", "Hello") Assert.assertEquals( - AppFunctions.Result( - "Message ID", + AppFunctions.SendMessageResult( "Message sent to: Alice Smith.", ), result, @@ -145,18 +146,17 @@ class AppFunctionsTest { } @Test - fun send_withImageUris_success() { + fun sendMessage_withImageUris_success() { runTest { val result = - appFunctions.send( + appFunctions.sendMessage( testContext, "1", "Hello", listOf(Uri.parse("content://media/1")), ) Assert.assertEquals( - AppFunctions.Result( - "Message ID", + AppFunctions.SendMessageResult( "Message sent to: Alice Smith.", ), result, @@ -165,12 +165,11 @@ class AppFunctionsTest { } @Test - fun send_toGroup_success() { + fun sendMessage_toGroup_success() { runTest { - val result = appFunctions.send(testContext, "g1", "Hello") + val result = appFunctions.sendMessage(testContext, "g1", "Hello") Assert.assertEquals( - AppFunctions.Result( - "Message ID", + AppFunctions.SendMessageResult( "Message sent to: Work Friends.", ), result, @@ -179,24 +178,24 @@ class AppFunctionsTest { } @Test(expected = AppFunctionInvalidArgumentException::class) - fun send_emptyContent_fails() { + fun sendMessage_emptyContent_fails() { runTest { - appFunctions.send(testContext, "1", "") + appFunctions.sendMessage(testContext, "1", "") } } @Test(expected = AppFunctionElementNotFoundException::class) - fun send_invalidRecipient_fails() { + fun sendMessage_invalidRecipient_fails() { runTest { - appFunctions.send(testContext, "nonexistent_id", "Hello") + appFunctions.sendMessage(testContext, "nonexistent_id", "Hello") } } @Test(expected = AppFunctionAppUnknownException::class) - fun send_repositoryError_returnsError() { + fun sendMessage_repositoryError_returnsError() { runTest { messageRepository.shouldFail = true - appFunctions.send(testContext, "1", "Hello") + appFunctions.sendMessage(testContext, "1", "Hello") } } @@ -216,4 +215,54 @@ class AppFunctionsTest { } } + @Test + fun searchMessages_allChats_returnsMatches() { + runTest { + messageRepository.saveMessage("1", DisplayMessage("Hello Alice", 1000, isInbound = true)) + messageRepository.saveMessage("2", DisplayMessage("Hello Bob", 2000, isInbound = false)) + messageRepository.saveMessage("1", DisplayMessage("How are you?", 3000, isInbound = false)) + + val results = appFunctions.searchMessages(testContext, "Hello") + + Assert.assertEquals(2, results.size) + + val aliceResult = results.first { it.endpointValue == "1" } + Assert.assertEquals(1, aliceResult.messages.size) + Assert.assertEquals("Hello Alice", aliceResult.messages[0].messageBody) + Assert.assertEquals(1000L, aliceResult.messages[0].timestamp) + Assert.assertFalse(aliceResult.messages[0].isSent) + + val bobResult = results.first { it.endpointValue == "2" } + Assert.assertEquals(1, bobResult.messages.size) + Assert.assertEquals("Hello Bob", bobResult.messages[0].messageBody) + Assert.assertEquals(2000L, bobResult.messages[0].timestamp) + Assert.assertTrue(bobResult.messages[0].isSent) + } + } + + @Test + fun searchMessages_specificChat_returnsMatches() { + runTest { + messageRepository.saveMessage("1", DisplayMessage("Hello Alice", 1000, isInbound = true)) + messageRepository.saveMessage("2", DisplayMessage("Hello Bob", 2000, isInbound = false)) + + val results = appFunctions.searchMessages(testContext, "Hello", endpointValue = "1") + + Assert.assertEquals(1, results.size) + Assert.assertEquals("1", results[0].endpointValue) + Assert.assertEquals("Hello Alice", results[0].messages[0].messageBody) + } + } + + @Test + fun searchMessages_noMatches_returnsEmpty() { + runTest { + messageRepository.saveMessage("1", DisplayMessage("Hello Alice", 1000, isInbound = true)) + + val results = appFunctions.searchMessages(testContext, "Goodbye") + + Assert.assertTrue(results.isEmpty()) + } + } + } diff --git a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index fdd153e..40f2758 100644 --- a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -132,6 +132,8 @@ class RecipientsRepository * @param maxCount Maximum number of results to return per entity type. * @return A unified list of [ContactSearchResult] containing both individuals and groups. */ + + fun searchAny( query: String?, maxCount: Int, @@ -141,7 +143,8 @@ class RecipientsRepository ContactSearchResult( endpointValue = it.id, endpointType = "INDIVIDUAL", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.email, ) } val groups = @@ -149,7 +152,8 @@ class RecipientsRepository ContactSearchResult( endpointValue = it.id, endpointType = "GROUP", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.name, ) } return mutableListOf().apply { diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 81a6574..2374e53 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -29,6 +29,7 @@ import com.example.chatapp.data.CallManager import com.example.chatapp.data.MessageRepository import com.example.chatapp.data.RecipientsRepository import javax.inject.Inject +import kotlinx.coroutines.flow.first /** * Provides functions for chat-related operations such as retrieving contacts and sending messages. @@ -46,23 +47,24 @@ class AppFunctions * @param appFunctionContext The context of this app function call. * @param query Search string for the contact or group name. Can be partial or full names. * If blank, returns the most recently contacted entities. - * @param filterType Filter results by entity type. Accepts "INDIVIDUAL" or "GROUP". + * @param contactType Filter results by entity type. Accepts "INDIVIDUAL", "GROUP", or "ANY". */ @AppFunction(isDescribedByKDoc = true) suspend fun searchContacts( appFunctionContext: AppFunctionContext, query: String, - @AppFunctionStringValueConstraint(enumValues = ["INDIVIDUAL", "GROUP"]) - filterType: String, + @AppFunctionStringValueConstraint(enumValues = ["INDIVIDUAL", "GROUP", "ANY"]) + contactType: String, ): List { val recipients = - when (filterType) { + when (contactType) { "INDIVIDUAL" -> { recipientsRepository.searchRecipients(query, 3).map { ContactSearchResult( endpointValue = it.id, endpointType = "INDIVIDUAL", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.email, ) } } @@ -71,23 +73,24 @@ class AppFunctions ContactSearchResult( endpointValue = it.id, endpointType = "GROUP", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.name, ) } } - else -> { + "ANY" -> { recipientsRepository.searchAny(query, maxCount = 3) - .ifEmpty { - throw AppFunctionInvalidArgumentException( - "Only INDIVIDUAL or GROUP are accepted filter arguments.", - ) - } + } + else -> { + throw AppFunctionInvalidArgumentException( + "Invalid contactType: $contactType", + ) } } if (recipients.isEmpty()) { throw AppFunctionInvalidArgumentException( - "$filterType with name $query not found. Ask the user to clarify the name", + "$contactType with name $query not found. Ask the user to clarify the name", ) } return recipients @@ -102,12 +105,12 @@ class AppFunctions * @param imageUris List of URIs for images to attach. */ @AppFunction(isDescribedByKDoc = true) - suspend fun send( + suspend fun sendMessage( appFunctionContext: AppFunctionContext, endpointValue: String, messageBody: String, imageUris: List? = null, - ): Result { + ): SendMessageResult { if (messageBody.isBlank()) { throw AppFunctionInvalidArgumentException("Message body cannot be empty") } @@ -131,7 +134,7 @@ class AppFunctions // 3. RETURN: Provide a confirmation string // The bot may use this string or the fact that it didn't throw to confirm success. - return Result(sentMessageId, "Message sent to: $displayName.") + return SendMessageResult("Message sent to: $displayName.") } /** @@ -165,6 +168,54 @@ class AppFunctions ) } + /** + * Search for messages containing a query string, optionally filtered by a specific recipient. + * + * @param appFunctionContext The context of this app function call. + * @param query The text to search for within message bodies. + * @param endpointValue Optional unique identifier of the contact or group to restrict the search to. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun searchMessages( + appFunctionContext: AppFunctionContext, + query: String, + endpointValue: String? = null, + ): List { + val targetIds = + if (endpointValue != null) { + listOf(endpointValue) + } else { + val individuals = recipientsRepository.getAllRecipients().map { it.id } + val groups = recipientsRepository.getAllGroups().map { it.id } + individuals + groups + } + + val results = mutableListOf() + + for (id in targetIds) { + val messages = messageRepository.getMessages(id).first() + val matchingMessages = + messages.filter { it.content.contains(query, ignoreCase = true) } + if (matchingMessages.isNotEmpty()) { + results.add( + MessagesSearchResult( + endpointValue = id, + messages = + matchingMessages.map { + Message( + messageBody = it.content, + timestamp = it.sentAt, + isSent = !it.isInbound, + ) + }, + ) + ) + } + } + + return results + } + /** Represents a result from a contact or group search. */ @AppFunctionSerializable(isDescribedByKDoc = true) data class ContactSearchResult( @@ -173,14 +224,14 @@ class AppFunctions /** The type of the found entity, either "INDIVIDUAL" or "GROUP". */ val endpointType: String, /** The human-readable name of the contact or group. */ - val displayName: String, + val contactDisplayName: String, + /** The human-readable name of the endpoint (e.g. email, phone number). */ + val endpointDisplayName: String, ) /** Result of a message sending operation. */ @AppFunctionSerializable(isDescribedByKDoc = true) - data class Result( - /** The unique identifier for the successfully sent message. */ - val messageId: String, + data class SendMessageResult( /** A human-readable status message indicating success. */ val message: String, ) @@ -206,4 +257,24 @@ class AppFunctions /** List of members belonging to the group. */ val recipients: List, ) + + /** Represents a message returned in search results. */ + @AppFunctionSerializable(isDescribedByKDoc = true) + data class Message( + /** The text content of the message. */ + val messageBody: String, + /** The timestamp when the message was sent or received. */ + val timestamp: Long, + /** True if the message was sent by the user, false if it was received. */ + val isSent: Boolean, + ) + + /** Represents the search results for a specific chat endpoint. */ + @AppFunctionSerializable(isDescribedByKDoc = true) + data class MessagesSearchResult( + /** The unique identifier of the contact or group. */ + val endpointValue: String, + /** The list of matching messages found in this chat. */ + val messages: List, + ) } diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index fdd153e..40f2758 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -132,6 +132,8 @@ class RecipientsRepository * @param maxCount Maximum number of results to return per entity type. * @return A unified list of [ContactSearchResult] containing both individuals and groups. */ + + fun searchAny( query: String?, maxCount: Int, @@ -141,7 +143,8 @@ class RecipientsRepository ContactSearchResult( endpointValue = it.id, endpointType = "INDIVIDUAL", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.email, ) } val groups = @@ -149,7 +152,8 @@ class RecipientsRepository ContactSearchResult( endpointValue = it.id, endpointType = "GROUP", - displayName = it.name, + contactDisplayName = it.name, + endpointDisplayName = it.name, ) } return mutableListOf().apply { From 1470be307354977c47f1667b9d3e25f74b704e7d Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 11:50:19 +0000 Subject: [PATCH 06/13] unnest string --- .../appfunctions/AppFunctionInstrumentationTest.kt | 3 +-- .../example/chatapp/appfunctions/AppFunctionsTest.kt | 12 +++--------- .../com/example/chatapp/appfunctions/AppFunctions.kt | 12 ++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt index 3ba0d73..2b0585b 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt @@ -91,8 +91,7 @@ class AppFunctionInstrumentationTest { val successResponse = assertIs(response) assertThat( successResponse.returnValue - .getAppFunctionData(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE) - ?.getString("message"), + .getString(ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE), ) .isEqualTo("Message sent to: Alice Smith.") // Verify that the message was actually saved in the repository diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 22b6ced..23c6c2a 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -137,9 +137,7 @@ class AppFunctionsTest { runTest { val result = appFunctions.sendMessage(testContext, "1", "Hello") Assert.assertEquals( - AppFunctions.SendMessageResult( - "Message sent to: Alice Smith.", - ), + "Message sent to: Alice Smith.", result, ) } @@ -156,9 +154,7 @@ class AppFunctionsTest { listOf(Uri.parse("content://media/1")), ) Assert.assertEquals( - AppFunctions.SendMessageResult( - "Message sent to: Alice Smith.", - ), + "Message sent to: Alice Smith.", result, ) } @@ -169,9 +165,7 @@ class AppFunctionsTest { runTest { val result = appFunctions.sendMessage(testContext, "g1", "Hello") Assert.assertEquals( - AppFunctions.SendMessageResult( - "Message sent to: Work Friends.", - ), + "Message sent to: Work Friends.", result, ) } diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 2374e53..3a7a450 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -103,6 +103,7 @@ class AppFunctions * @param endpointValue The unique identifier for the recipient or group. * @param messageBody The text content of the message. Cannot be empty. * @param imageUris List of URIs for images to attach. + * @return A human-readable message indicating the error or confirmation. */ @AppFunction(isDescribedByKDoc = true) suspend fun sendMessage( @@ -110,7 +111,7 @@ class AppFunctions endpointValue: String, messageBody: String, imageUris: List? = null, - ): SendMessageResult { + ): String { if (messageBody.isBlank()) { throw AppFunctionInvalidArgumentException("Message body cannot be empty") } @@ -134,7 +135,7 @@ class AppFunctions // 3. RETURN: Provide a confirmation string // The bot may use this string or the fact that it didn't throw to confirm success. - return SendMessageResult("Message sent to: $displayName.") + return "Message sent to: $displayName." } /** @@ -229,12 +230,7 @@ class AppFunctions val endpointDisplayName: String, ) - /** Result of a message sending operation. */ - @AppFunctionSerializable(isDescribedByKDoc = true) - data class SendMessageResult( - /** A human-readable status message indicating success. */ - val message: String, - ) + /** Represents a recipient of a message. */ @AppFunctionSerializable(isDescribedByKDoc = true) From 7238b29598aa0e28948c3747cffa061296e19343 Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 11:57:53 +0000 Subject: [PATCH 07/13] add sender name --- .../chatapp/appfunctions/AppFunctionsTest.kt | 4 ++-- .../example/chatapp/appfunctions/AppFunctions.kt | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 23c6c2a..8bacfe7 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -224,13 +224,13 @@ class AppFunctionsTest { Assert.assertEquals(1, aliceResult.messages.size) Assert.assertEquals("Hello Alice", aliceResult.messages[0].messageBody) Assert.assertEquals(1000L, aliceResult.messages[0].timestamp) - Assert.assertFalse(aliceResult.messages[0].isSent) + Assert.assertEquals("Alice Smith", aliceResult.messages[0].senderDisplayName) val bobResult = results.first { it.endpointValue == "2" } Assert.assertEquals(1, bobResult.messages.size) Assert.assertEquals("Hello Bob", bobResult.messages[0].messageBody) Assert.assertEquals(2000L, bobResult.messages[0].timestamp) - Assert.assertTrue(bobResult.messages[0].isSent) + Assert.assertEquals("Me", bobResult.messages[0].senderDisplayName) } } diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 3a7a450..d4573df 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -203,10 +203,18 @@ class AppFunctions endpointValue = id, messages = matchingMessages.map { + val senderDisplayName = it.senderName + ?: if (it.isInbound) { + recipientsRepository.getRecipientById(id)?.name + ?: recipientsRepository.getGroupById(id)?.name + ?: "Other" + } else { + "Me" + } Message( messageBody = it.content, timestamp = it.sentAt, - isSent = !it.isInbound, + senderDisplayName = senderDisplayName, ) }, ) @@ -261,8 +269,8 @@ class AppFunctions val messageBody: String, /** The timestamp when the message was sent or received. */ val timestamp: Long, - /** True if the message was sent by the user, false if it was received. */ - val isSent: Boolean, + /** The human-readable name of the sender. */ + val senderDisplayName: String, ) /** Represents the search results for a specific chat endpoint. */ From e4c0c4981830e3bf011f86d848403b28158caa13 Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 11:59:43 +0000 Subject: [PATCH 08/13] javadoc --- .../kotlin/com/example/chatapp/appfunctions/AppFunctions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index d4573df..8ca6f8d 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -278,7 +278,7 @@ class AppFunctions data class MessagesSearchResult( /** The unique identifier of the contact or group. */ val endpointValue: String, - /** The list of matching messages found in this chat. */ + /** The list of relevant messages found in this chat. */ val messages: List, ) } From 462744890bae4022d9ee7ad4ea9175c3032c260d Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 12:23:09 +0000 Subject: [PATCH 09/13] nest contact --- .../AppFunctionInstrumentationTest.kt | 10 ++-- .../chatapp/appfunctions/AppFunctionsTest.kt | 30 +++++++++-- .../chatapp/data/RecipientsRepository.kt | 54 +++++++++++-------- .../chatapp/appfunctions/AppFunctions.kt | 36 +++++++------ .../chatapp/data/RecipientsRepository.kt | 54 +++++++++++-------- 5 files changed, 118 insertions(+), 66 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt index 2b0585b..64ce697 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt @@ -167,10 +167,14 @@ class AppFunctionInstrumentationTest { ) .containsExactly( AppFunctions.ContactSearchResult( - endpointValue = "1", - endpointType = "INDIVIDUAL", contactDisplayName = "Alice Smith", - endpointDisplayName = "alice@example.com", + endpointType = "INDIVIDUAL", + endpoints = listOf( + AppFunctions.Endpoint( + endpointValue = "1", + endpointDisplayName = "alice@example.com", + ) + ) ), ) } diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 8bacfe7..4b09c88 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -100,7 +100,10 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Alice", "INDIVIDUAL") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) - Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) + Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals(1, contacts[0].endpoints.size) + Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) + Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) } } @@ -110,7 +113,10 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Work", "GROUP") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Work Friends", contacts[0].contactDisplayName) - Assert.assertEquals("Work Friends", contacts[0].endpointDisplayName) + Assert.assertEquals("GROUP", contacts[0].endpointType) + Assert.assertEquals(1, contacts[0].endpoints.size) + Assert.assertEquals("g1", contacts[0].endpoints[0].endpointValue) + Assert.assertEquals("Work Friends", contacts[0].endpoints[0].endpointDisplayName) } } @@ -128,7 +134,25 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Alice", "ANY") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) - Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) + Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals(1, contacts[0].endpoints.size) + Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) + Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) + } + } + + @Test + fun searchContacts_duplicateNamesMerged() { + runBlocking { + val contacts = appFunctions.searchContacts(testContext, "Bob", "INDIVIDUAL") + Assert.assertEquals(1, contacts.size) + Assert.assertEquals("Bob Johnson", contacts[0].contactDisplayName) + Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals(2, contacts[0].endpoints.size) + Assert.assertEquals("2", contacts[0].endpoints[0].endpointValue) + Assert.assertEquals("bob@example.com", contacts[0].endpoints[0].endpointDisplayName) + Assert.assertEquals("7", contacts[0].endpoints[1].endpointValue) + Assert.assertEquals("bob2@example.com", contacts[0].endpoints[1].endpointDisplayName) } } diff --git a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index 40f2758..ba4f3c9 100644 --- a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -18,6 +18,7 @@ package com.example.chatapp.data import com.example.chatapp.appfunctions.AppFunctions.ChatGroup import com.example.chatapp.appfunctions.AppFunctions.ContactSearchResult import com.example.chatapp.appfunctions.AppFunctions.Recipient +import com.example.chatapp.appfunctions.AppFunctions.Endpoint import javax.inject.Inject import javax.inject.Singleton @@ -87,18 +88,31 @@ class RecipientsRepository fun searchRecipients( query: String?, maxCount: Int, - ): List { - if (query.isNullOrBlank()) { - // TODO:Return most recently contacted. - return recipients.take(maxCount) + ): List { + val matched = if (query.isNullOrBlank()) { + recipients + } else { + recipients.filter { + it.name.contains(query, ignoreCase = true) || + it.email.contains( + query, + ignoreCase = true, + ) + } } - return recipients.filter { - it.name.contains(query, ignoreCase = true) || - it.email.contains( - query, - ignoreCase = true, - ) + val grouped = matched.groupBy { it.name }.map { (name, list) -> + ContactSearchResult( + contactDisplayName = name, + endpointType = "INDIVIDUAL", + endpoints = list.map { Endpoint(it.id, it.email) } + ) + } + + return if (query.isNullOrBlank()) { + grouped.take(maxCount) + } else { + grouped } } @@ -138,22 +152,18 @@ class RecipientsRepository query: String?, maxCount: Int, ): List { - val individuals = - searchRecipients(query, maxCount).map { - ContactSearchResult( - endpointValue = it.id, - endpointType = "INDIVIDUAL", - contactDisplayName = it.name, - endpointDisplayName = it.email, - ) - } + val individuals = searchRecipients(query, maxCount) val groups = searchGroups(query, maxCount).map { ContactSearchResult( - endpointValue = it.id, - endpointType = "GROUP", contactDisplayName = it.name, - endpointDisplayName = it.name, + endpointType = "GROUP", + endpoints = listOf( + Endpoint( + endpointValue = it.id, + endpointDisplayName = it.name, + ) + ) ) } return mutableListOf().apply { diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 8ca6f8d..24e01a1 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -59,22 +59,19 @@ class AppFunctions val recipients = when (contactType) { "INDIVIDUAL" -> { - recipientsRepository.searchRecipients(query, 3).map { - ContactSearchResult( - endpointValue = it.id, - endpointType = "INDIVIDUAL", - contactDisplayName = it.name, - endpointDisplayName = it.email, - ) - } + recipientsRepository.searchRecipients(query, 3) } "GROUP" -> { recipientsRepository.searchGroups(query, 3).map { ContactSearchResult( - endpointValue = it.id, - endpointType = "GROUP", contactDisplayName = it.name, - endpointDisplayName = it.name, + endpointType = "GROUP", + endpoints = listOf( + Endpoint( + endpointValue = it.id, + endpointDisplayName = it.name, + ) + ) ) } } @@ -228,13 +225,20 @@ class AppFunctions /** Represents a result from a contact or group search. */ @AppFunctionSerializable(isDescribedByKDoc = true) data class ContactSearchResult( - /** The unique identifier. */ - val endpointValue: String, - /** The type of the found entity, either "INDIVIDUAL" or "GROUP". */ - val endpointType: String, /** The human-readable name of the contact or group. */ val contactDisplayName: String, - /** The human-readable name of the endpoint (e.g. email, phone number). */ + /** The type of the found entity, either "INDIVIDUAL" or "GROUP". */ + val endpointType: String, + /** The list of endpoints associated with this contact or group. */ + val endpoints: List, + ) + + /** Represents an endpoint of a contact or group. */ + @AppFunctionSerializable(isDescribedByKDoc = true) + data class Endpoint( + /** The unique identifier of the endpoint. */ + val endpointValue: String, + /** The human-readable label/display name of the endpoint. */ val endpointDisplayName: String, ) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index 40f2758..ba4f3c9 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -18,6 +18,7 @@ package com.example.chatapp.data import com.example.chatapp.appfunctions.AppFunctions.ChatGroup import com.example.chatapp.appfunctions.AppFunctions.ContactSearchResult import com.example.chatapp.appfunctions.AppFunctions.Recipient +import com.example.chatapp.appfunctions.AppFunctions.Endpoint import javax.inject.Inject import javax.inject.Singleton @@ -87,18 +88,31 @@ class RecipientsRepository fun searchRecipients( query: String?, maxCount: Int, - ): List { - if (query.isNullOrBlank()) { - // TODO:Return most recently contacted. - return recipients.take(maxCount) + ): List { + val matched = if (query.isNullOrBlank()) { + recipients + } else { + recipients.filter { + it.name.contains(query, ignoreCase = true) || + it.email.contains( + query, + ignoreCase = true, + ) + } } - return recipients.filter { - it.name.contains(query, ignoreCase = true) || - it.email.contains( - query, - ignoreCase = true, - ) + val grouped = matched.groupBy { it.name }.map { (name, list) -> + ContactSearchResult( + contactDisplayName = name, + endpointType = "INDIVIDUAL", + endpoints = list.map { Endpoint(it.id, it.email) } + ) + } + + return if (query.isNullOrBlank()) { + grouped.take(maxCount) + } else { + grouped } } @@ -138,22 +152,18 @@ class RecipientsRepository query: String?, maxCount: Int, ): List { - val individuals = - searchRecipients(query, maxCount).map { - ContactSearchResult( - endpointValue = it.id, - endpointType = "INDIVIDUAL", - contactDisplayName = it.name, - endpointDisplayName = it.email, - ) - } + val individuals = searchRecipients(query, maxCount) val groups = searchGroups(query, maxCount).map { ContactSearchResult( - endpointValue = it.id, - endpointType = "GROUP", contactDisplayName = it.name, - endpointDisplayName = it.name, + endpointType = "GROUP", + endpoints = listOf( + Endpoint( + endpointValue = it.id, + endpointDisplayName = it.name, + ) + ) ) } return mutableListOf().apply { From 261e32c21fb55437cb501e04c8faeeeb3c140daa Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 12:26:22 +0000 Subject: [PATCH 10/13] throw if empty query --- .../com/example/chatapp/appfunctions/AppFunctionsTest.kt | 7 +++---- .../com/example/chatapp/appfunctions/AppFunctions.kt | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 4b09c88..6dab78b 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -120,11 +120,10 @@ class AppFunctionsTest { } } - @Test - fun searchContacts_emptyQuery_returnsRecent() { + @Test(expected = AppFunctionInvalidArgumentException::class) + fun searchContacts_emptyQuery_fails() { runBlocking { - val contacts = appFunctions.searchContacts(testContext, "", "INDIVIDUAL") - Assert.assertEquals(3, contacts.size) + appFunctions.searchContacts(testContext, "", "INDIVIDUAL") } } diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 24e01a1..a4d0ab6 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -56,6 +56,9 @@ class AppFunctions @AppFunctionStringValueConstraint(enumValues = ["INDIVIDUAL", "GROUP", "ANY"]) contactType: String, ): List { + if (query.isBlank()) { + throw AppFunctionInvalidArgumentException("Query cannot be empty") + } val recipients = when (contactType) { "INDIVIDUAL" -> { From 9323b2e3307f69251fbd49ae86f6421a79be73a5 Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 12:32:49 +0000 Subject: [PATCH 11/13] rename --- .../appfunctions/AppFunctionInstrumentationTest.kt | 2 +- .../com/example/chatapp/appfunctions/AppFunctionsTest.kt | 8 ++++---- .../com/example/chatapp/data/RecipientsRepository.kt | 4 ++-- .../com/example/chatapp/appfunctions/AppFunctions.kt | 8 ++++---- .../com/example/chatapp/data/RecipientsRepository.kt | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt index 64ce697..0981090 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt @@ -168,7 +168,7 @@ class AppFunctionInstrumentationTest { .containsExactly( AppFunctions.ContactSearchResult( contactDisplayName = "Alice Smith", - endpointType = "INDIVIDUAL", + contactType = "INDIVIDUAL", endpoints = listOf( AppFunctions.Endpoint( endpointValue = "1", diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 6dab78b..d0b52dd 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -100,7 +100,7 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Alice", "INDIVIDUAL") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) - Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) Assert.assertEquals(1, contacts[0].endpoints.size) Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) @@ -113,7 +113,7 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Work", "GROUP") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Work Friends", contacts[0].contactDisplayName) - Assert.assertEquals("GROUP", contacts[0].endpointType) + Assert.assertEquals("GROUP", contacts[0].contactType) Assert.assertEquals(1, contacts[0].endpoints.size) Assert.assertEquals("g1", contacts[0].endpoints[0].endpointValue) Assert.assertEquals("Work Friends", contacts[0].endpoints[0].endpointDisplayName) @@ -133,7 +133,7 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Alice", "ANY") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) - Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) Assert.assertEquals(1, contacts[0].endpoints.size) Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) @@ -146,7 +146,7 @@ class AppFunctionsTest { val contacts = appFunctions.searchContacts(testContext, "Bob", "INDIVIDUAL") Assert.assertEquals(1, contacts.size) Assert.assertEquals("Bob Johnson", contacts[0].contactDisplayName) - Assert.assertEquals("INDIVIDUAL", contacts[0].endpointType) + Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) Assert.assertEquals(2, contacts[0].endpoints.size) Assert.assertEquals("2", contacts[0].endpoints[0].endpointValue) Assert.assertEquals("bob@example.com", contacts[0].endpoints[0].endpointDisplayName) diff --git a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index ba4f3c9..cd4a25c 100644 --- a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -104,7 +104,7 @@ class RecipientsRepository val grouped = matched.groupBy { it.name }.map { (name, list) -> ContactSearchResult( contactDisplayName = name, - endpointType = "INDIVIDUAL", + contactType = "INDIVIDUAL", endpoints = list.map { Endpoint(it.id, it.email) } ) } @@ -157,7 +157,7 @@ class RecipientsRepository searchGroups(query, maxCount).map { ContactSearchResult( contactDisplayName = it.name, - endpointType = "GROUP", + contactType = "GROUP", endpoints = listOf( Endpoint( endpointValue = it.id, diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index a4d0ab6..39bf38c 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -45,9 +45,9 @@ class AppFunctions * Search for contacts or groups by name. * * @param appFunctionContext The context of this app function call. - * @param query Search string for the contact or group name. Can be partial or full names. - * If blank, returns the most recently contacted entities. + * @param query Search string for the contact or group name. Can be partial or full names. Throws [AppFunctionInvalidArgumentException] if empty. * @param contactType Filter results by entity type. Accepts "INDIVIDUAL", "GROUP", or "ANY". + * @throws AppFunctionInvalidArgumentException if the query is blank or no matching contacts/groups are found. */ @AppFunction(isDescribedByKDoc = true) suspend fun searchContacts( @@ -68,7 +68,7 @@ class AppFunctions recipientsRepository.searchGroups(query, 3).map { ContactSearchResult( contactDisplayName = it.name, - endpointType = "GROUP", + contactType = "GROUP", endpoints = listOf( Endpoint( endpointValue = it.id, @@ -231,7 +231,7 @@ class AppFunctions /** The human-readable name of the contact or group. */ val contactDisplayName: String, /** The type of the found entity, either "INDIVIDUAL" or "GROUP". */ - val endpointType: String, + val contactType: String, /** The list of endpoints associated with this contact or group. */ val endpoints: List, ) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index ba4f3c9..cd4a25c 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -104,7 +104,7 @@ class RecipientsRepository val grouped = matched.groupBy { it.name }.map { (name, list) -> ContactSearchResult( contactDisplayName = name, - endpointType = "INDIVIDUAL", + contactType = "INDIVIDUAL", endpoints = list.map { Endpoint(it.id, it.email) } ) } @@ -157,7 +157,7 @@ class RecipientsRepository searchGroups(query, maxCount).map { ContactSearchResult( contactDisplayName = it.name, - endpointType = "GROUP", + contactType = "GROUP", endpoints = listOf( Endpoint( endpointValue = it.id, From 1c06f4d8875c8114b3ebdbcfed22742a20305bcf Mon Sep 17 00:00:00 2001 From: Corina Date: Tue, 7 Jul 2026 13:32:04 +0000 Subject: [PATCH 12/13] revert nest --- .../AppFunctionInstrumentationTest.kt | 8 ++--- .../chatapp/appfunctions/AppFunctionsTest.kt | 32 +++++++++---------- .../chatapp/data/RecipientsRepository.kt | 20 +++++------- .../chatapp/appfunctions/AppFunctions.kt | 15 ++------- .../chatapp/data/RecipientsRepository.kt | 20 +++++------- 5 files changed, 36 insertions(+), 59 deletions(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt index 0981090..1f5b120 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionInstrumentationTest.kt @@ -169,12 +169,8 @@ class AppFunctionInstrumentationTest { AppFunctions.ContactSearchResult( contactDisplayName = "Alice Smith", contactType = "INDIVIDUAL", - endpoints = listOf( - AppFunctions.Endpoint( - endpointValue = "1", - endpointDisplayName = "alice@example.com", - ) - ) + endpointValue = "1", + endpointDisplayName = "alice@example.com", ), ) } diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index d0b52dd..7ad8ea5 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -101,9 +101,8 @@ class AppFunctionsTest { Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) - Assert.assertEquals(1, contacts[0].endpoints.size) - Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) - Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) + Assert.assertEquals("1", contacts[0].endpointValue) + Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) } } @@ -114,9 +113,8 @@ class AppFunctionsTest { Assert.assertEquals(1, contacts.size) Assert.assertEquals("Work Friends", contacts[0].contactDisplayName) Assert.assertEquals("GROUP", contacts[0].contactType) - Assert.assertEquals(1, contacts[0].endpoints.size) - Assert.assertEquals("g1", contacts[0].endpoints[0].endpointValue) - Assert.assertEquals("Work Friends", contacts[0].endpoints[0].endpointDisplayName) + Assert.assertEquals("g1", contacts[0].endpointValue) + Assert.assertEquals("Work Friends", contacts[0].endpointDisplayName) } } @@ -134,24 +132,26 @@ class AppFunctionsTest { Assert.assertEquals(1, contacts.size) Assert.assertEquals("Alice Smith", contacts[0].contactDisplayName) Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) - Assert.assertEquals(1, contacts[0].endpoints.size) - Assert.assertEquals("1", contacts[0].endpoints[0].endpointValue) - Assert.assertEquals("alice@example.com", contacts[0].endpoints[0].endpointDisplayName) + Assert.assertEquals("1", contacts[0].endpointValue) + Assert.assertEquals("alice@example.com", contacts[0].endpointDisplayName) } } @Test - fun searchContacts_duplicateNamesMerged() { + fun searchContacts_duplicateNamesReturnedAsSeparate() { runBlocking { val contacts = appFunctions.searchContacts(testContext, "Bob", "INDIVIDUAL") - Assert.assertEquals(1, contacts.size) + Assert.assertEquals(2, contacts.size) + Assert.assertEquals("Bob Johnson", contacts[0].contactDisplayName) Assert.assertEquals("INDIVIDUAL", contacts[0].contactType) - Assert.assertEquals(2, contacts[0].endpoints.size) - Assert.assertEquals("2", contacts[0].endpoints[0].endpointValue) - Assert.assertEquals("bob@example.com", contacts[0].endpoints[0].endpointDisplayName) - Assert.assertEquals("7", contacts[0].endpoints[1].endpointValue) - Assert.assertEquals("bob2@example.com", contacts[0].endpoints[1].endpointDisplayName) + Assert.assertEquals("2", contacts[0].endpointValue) + Assert.assertEquals("bob@example.com", contacts[0].endpointDisplayName) + + Assert.assertEquals("Bob Johnson", contacts[1].contactDisplayName) + Assert.assertEquals("INDIVIDUAL", contacts[1].contactType) + Assert.assertEquals("7", contacts[1].endpointValue) + Assert.assertEquals("bob2@example.com", contacts[1].endpointDisplayName) } } diff --git a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index cd4a25c..85b33da 100644 --- a/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/app/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -18,7 +18,6 @@ package com.example.chatapp.data import com.example.chatapp.appfunctions.AppFunctions.ChatGroup import com.example.chatapp.appfunctions.AppFunctions.ContactSearchResult import com.example.chatapp.appfunctions.AppFunctions.Recipient -import com.example.chatapp.appfunctions.AppFunctions.Endpoint import javax.inject.Inject import javax.inject.Singleton @@ -101,18 +100,19 @@ class RecipientsRepository } } - val grouped = matched.groupBy { it.name }.map { (name, list) -> + val mapped = matched.map { ContactSearchResult( - contactDisplayName = name, + contactDisplayName = it.name, contactType = "INDIVIDUAL", - endpoints = list.map { Endpoint(it.id, it.email) } + endpointValue = it.id, + endpointDisplayName = it.email, ) } return if (query.isNullOrBlank()) { - grouped.take(maxCount) + mapped.take(maxCount) } else { - grouped + mapped } } @@ -158,12 +158,8 @@ class RecipientsRepository ContactSearchResult( contactDisplayName = it.name, contactType = "GROUP", - endpoints = listOf( - Endpoint( - endpointValue = it.id, - endpointDisplayName = it.name, - ) - ) + endpointValue = it.id, + endpointDisplayName = it.name, ) } return mutableListOf().apply { diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index 39bf38c..bcc0da1 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -69,12 +69,8 @@ class AppFunctions ContactSearchResult( contactDisplayName = it.name, contactType = "GROUP", - endpoints = listOf( - Endpoint( - endpointValue = it.id, - endpointDisplayName = it.name, - ) - ) + endpointValue = it.id, + endpointDisplayName = it.name, ) } } @@ -232,13 +228,6 @@ class AppFunctions val contactDisplayName: String, /** The type of the found entity, either "INDIVIDUAL" or "GROUP". */ val contactType: String, - /** The list of endpoints associated with this contact or group. */ - val endpoints: List, - ) - - /** Represents an endpoint of a contact or group. */ - @AppFunctionSerializable(isDescribedByKDoc = true) - data class Endpoint( /** The unique identifier of the endpoint. */ val endpointValue: String, /** The human-readable label/display name of the endpoint. */ diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt index cd4a25c..85b33da 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/data/RecipientsRepository.kt @@ -18,7 +18,6 @@ package com.example.chatapp.data import com.example.chatapp.appfunctions.AppFunctions.ChatGroup import com.example.chatapp.appfunctions.AppFunctions.ContactSearchResult import com.example.chatapp.appfunctions.AppFunctions.Recipient -import com.example.chatapp.appfunctions.AppFunctions.Endpoint import javax.inject.Inject import javax.inject.Singleton @@ -101,18 +100,19 @@ class RecipientsRepository } } - val grouped = matched.groupBy { it.name }.map { (name, list) -> + val mapped = matched.map { ContactSearchResult( - contactDisplayName = name, + contactDisplayName = it.name, contactType = "INDIVIDUAL", - endpoints = list.map { Endpoint(it.id, it.email) } + endpointValue = it.id, + endpointDisplayName = it.email, ) } return if (query.isNullOrBlank()) { - grouped.take(maxCount) + mapped.take(maxCount) } else { - grouped + mapped } } @@ -158,12 +158,8 @@ class RecipientsRepository ContactSearchResult( contactDisplayName = it.name, contactType = "GROUP", - endpoints = listOf( - Endpoint( - endpointValue = it.id, - endpointDisplayName = it.name, - ) - ) + endpointValue = it.id, + endpointDisplayName = it.name, ) } return mutableListOf().apply { From 44e96d516a56941f7bd345174f9494cc87fb1369 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 9 Jul 2026 12:28:11 +0000 Subject: [PATCH 13/13] empty check --- .../com/example/chatapp/appfunctions/AppFunctionsTest.kt | 8 ++++++++ .../com/example/chatapp/appfunctions/AppFunctions.kt | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt index 7ad8ea5..1d8a498 100644 --- a/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt +++ b/ChatApp/app/src/androidTest/kotlin/com/example/chatapp/appfunctions/AppFunctionsTest.kt @@ -282,4 +282,12 @@ class AppFunctionsTest { } } + @Test(expected = AppFunctionInvalidArgumentException::class) + fun searchMessages_emptyQuery_fails() { + runTest { + appFunctions.searchMessages(testContext, "") + } + } + } + diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt index bcc0da1..05ac2b9 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/AppFunctions.kt @@ -169,8 +169,9 @@ class AppFunctions * Search for messages containing a query string, optionally filtered by a specific recipient. * * @param appFunctionContext The context of this app function call. - * @param query The text to search for within message bodies. + * @param query The text to search for within message bodies. Throws [AppFunctionInvalidArgumentException] if empty. * @param endpointValue Optional unique identifier of the contact or group to restrict the search to. + * @throws AppFunctionInvalidArgumentException if the query is blank. */ @AppFunction(isDescribedByKDoc = true) suspend fun searchMessages( @@ -178,6 +179,9 @@ class AppFunctions query: String, endpointValue: String? = null, ): List { + if (query.isBlank()) { + throw AppFunctionInvalidArgumentException("Query cannot be empty") + } val targetIds = if (endpointValue != null) { listOf(endpointValue)