From 72399f8a3add9a9d3d5a3e7e9f69fe3f10d849ca Mon Sep 17 00:00:00 2001 From: corinac123 Date: Thu, 2 Jul 2026 15:18:22 +0100 Subject: [PATCH 1/5] Add endpoint to bob * Add an endpoint for Bob * Repository returns all recipients with same name From e0f5be034c75000a5eadeb443fe5310290133347 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 3 Sep 2026 17:02:42 +0000 Subject: [PATCH 2/5] add preconf function --- ChatApp/app/build.gradle.kts | 4 +- .../BaseChatAppFunctionService.kt | 82 +++++++++++++++++++ .../com/example/chatapp/appfunctions/Util.kt | 15 ++++ ChatApp/wear/build.gradle.kts | 4 +- 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/ChatApp/app/build.gradle.kts b/ChatApp/app/build.gradle.kts index f6e71559..a24229ac 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/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt index 3ed0ead1..a8ae248f 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt @@ -26,6 +26,7 @@ import androidx.appfunctions.AppFunctionInvalidArgumentException import androidx.appfunctions.AppFunctionService import androidx.appfunctions.AppFunctionServiceEntryPoint import androidx.appfunctions.AppFunctionStringValueConstraint +import androidx.appfunctions.AppFunctionTextResource import com.example.chatapp.data.CallManager import com.example.chatapp.data.DisplayMessage import com.example.chatapp.data.MessageRepository @@ -34,6 +35,8 @@ import com.example.chatapp.data.WallpaperRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.first +import org.json.JSONArray +import org.json.JSONObject import javax.inject.Inject /** @@ -105,6 +108,85 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { return recipients } + /** + * Fetch details for sending a message before execution. + * Required workflow: Call "searchContacts" first to obtain a valid endpointValue. + * + * @param endpointValue The unique identifier for the recipient or group obtained from searchContacts. + * @param messageBody The text content of the message to send. Cannot be empty or blank. + * @param imageUris Optional list of image URIs to attach to the message. + * @return A [Result] object containing confirmation details. + * @throws AppFunctionInvalidArgumentException If messageBody is empty or blank. If thrown, ask the user to provide the message content to send. + * @throws AppFunctionElementNotFoundException If no contact or group matches endpointValue. If thrown, call "searchContacts" to find the correct ID. + */ + @AppFunction(isDescribedByKDoc = true) + suspend fun fetchSendMessageDetails( + endpointValue: String, + messageBody: String, + imageUris: List? = null, + ): Result { + if (messageBody.isBlank()) { + throw AppFunctionInvalidArgumentException("Message body cannot be empty") + } + val displayName = + recipientsRepository.getRecipientById(endpointValue)?.name + ?: recipientsRepository.getGroupById(endpointValue)?.name + ?: throw AppFunctionElementNotFoundException( + "No contact or group found for endpointValue: $endpointValue", + ) + + val sentMessageId = endpointValue + val surfaceId = "surface_$sentMessageId" + val catalogId = + "https://developers.google.com/connected-apps/a2ui/v0_9/comms_catalog_preview.json" + + val createSurface = + JSONObject().apply { + put( + "createSurface", + JSONObject().apply { + put("surfaceId", surfaceId) + put("catalogId", catalogId) + }, + ) + } + + val confirmationComponent = + JSONObject().apply { + put("component", "MessageConfirmationComponent") + put("id", "root") + put("contactDisplayName", displayName) + put("endpointDisplayName", "ChatApp") + put("messageBody", messageBody) + put("editMessageUri", "app://chatapp/edit/$sentMessageId") + } + + val updateComponents = + JSONObject().apply { + put( + "updateComponents", + JSONObject().apply { + put("surfaceId", surfaceId) + put("components", JSONArray().put(confirmationComponent).toString()) + }, + ) + } + + val a2uiJsonPayload = + JSONArray().apply { + put(createSurface) + put(updateComponents) + }.toString() + + val a2uiResource = + AppFunctionTextResource( + mimeType = "application/a2ui+json", + content = a2uiJsonPayload, + ) + + return Result(sentMessageId, "Message sent.", listOf(a2uiResource)) + } + /** * Send a text message with optional image attachments to an individual contact or group. * Required workflow: Call "searchContacts" first to obtain a valid endpointValue. diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt index ac8bee19..d841562b 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt @@ -15,7 +15,9 @@ */ package com.example.chatapp.appfunctions +import androidx.appfunctions.AppFunctionResourceContainer import androidx.appfunctions.AppFunctionSerializable +import androidx.appfunctions.AppFunctionTextResource /** * Represents a result from a contact or group search. @@ -32,6 +34,19 @@ data class ContactSearchResult( val endpointDisplayName: String, ) +/** + * Result of a message sending operation. + */ +@AppFunctionSerializable(isDescribedByKDoc = true) +data class Result( + /** The unique identifier for the successfully sent message or endpoint. */ + val messageId: String, + /** A human-readable status message confirming action details. */ + val message: String, + /** Optional list of text resources. */ + override val resources: List = emptyList(), +) : AppFunctionResourceContainer + /** * Represents an individual recipient or contact. */ diff --git a/ChatApp/wear/build.gradle.kts b/ChatApp/wear/build.gradle.kts index 4ff1d88a..d49784d9 100644 --- a/ChatApp/wear/build.gradle.kts +++ b/ChatApp/wear/build.gradle.kts @@ -29,8 +29,8 @@ android { applicationId = "com.example.chatapp.wear" minSdk = 36 targetSdk = 37 - versionCode = 1 - versionName = "1.0" + versionCode = 99999 + versionName = "99999" } buildTypes { From ddef0f1acefcc37c5a0671a905c8ade49d80bbd8 Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 1 Oct 2026 15:15:18 +0000 Subject: [PATCH 3/5] Revert ChatApp version to 1.0 --- ChatApp/app/build.gradle.kts | 4 ++-- ChatApp/wear/build.gradle.kts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ChatApp/app/build.gradle.kts b/ChatApp/app/build.gradle.kts index a24229ac..f6e71559 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" } diff --git a/ChatApp/wear/build.gradle.kts b/ChatApp/wear/build.gradle.kts index d49784d9..4ff1d88a 100644 --- a/ChatApp/wear/build.gradle.kts +++ b/ChatApp/wear/build.gradle.kts @@ -29,8 +29,8 @@ android { applicationId = "com.example.chatapp.wear" minSdk = 36 targetSdk = 37 - versionCode = 99999 - versionName = "99999" + versionCode = 1 + versionName = "1.0" } buildTypes { From 63e42b355f71025e5ff7e4d8731c7c3ef1b144df Mon Sep 17 00:00:00 2001 From: Corina Date: Thu, 1 Oct 2026 16:43:39 +0000 Subject: [PATCH 4/5] Address PR review comments: use ConfirmationPreview and endpointValue --- .../appfunctions/BaseChatAppFunctionService.kt | 11 +++++------ .../kotlin/com/example/chatapp/appfunctions/Util.kt | 12 ++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt index a8ae248f..11b30314 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt @@ -115,7 +115,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @param endpointValue The unique identifier for the recipient or group obtained from searchContacts. * @param messageBody The text content of the message to send. Cannot be empty or blank. * @param imageUris Optional list of image URIs to attach to the message. - * @return A [Result] object containing confirmation details. + * @return A [ConfirmationPreview] object containing confirmation details. * @throws AppFunctionInvalidArgumentException If messageBody is empty or blank. If thrown, ask the user to provide the message content to send. * @throws AppFunctionElementNotFoundException If no contact or group matches endpointValue. If thrown, call "searchContacts" to find the correct ID. */ @@ -124,7 +124,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { endpointValue: String, messageBody: String, imageUris: List? = null, - ): Result { + ): ConfirmationPreview { if (messageBody.isBlank()) { throw AppFunctionInvalidArgumentException("Message body cannot be empty") } @@ -135,8 +135,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { "No contact or group found for endpointValue: $endpointValue", ) - val sentMessageId = endpointValue - val surfaceId = "surface_$sentMessageId" + val surfaceId = "surface_$endpointValue" val catalogId = "https://developers.google.com/connected-apps/a2ui/v0_9/comms_catalog_preview.json" @@ -158,7 +157,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { put("contactDisplayName", displayName) put("endpointDisplayName", "ChatApp") put("messageBody", messageBody) - put("editMessageUri", "app://chatapp/edit/$sentMessageId") + put("editMessageUri", "app://chatapp/edit/$endpointValue") } val updateComponents = @@ -184,7 +183,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { content = a2uiJsonPayload, ) - return Result(sentMessageId, "Message sent.", listOf(a2uiResource)) + return ConfirmationPreview(resources = listOf(a2uiResource)) } /** diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt index d841562b..6406856a 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/Util.kt @@ -35,16 +35,12 @@ data class ContactSearchResult( ) /** - * Result of a message sending operation. + * Confirmation-card content for Gemini. */ @AppFunctionSerializable(isDescribedByKDoc = true) -data class Result( - /** The unique identifier for the successfully sent message or endpoint. */ - val messageId: String, - /** A human-readable status message confirming action details. */ - val message: String, - /** Optional list of text resources. */ - override val resources: List = emptyList(), +data class ConfirmationPreview( + /** Exactly one A2UI resource (mimeType "application/a2ui+json"). */ + override val resources: List, ) : AppFunctionResourceContainer /** From 9cbe31aac6ef12961ebe8d9b369c55d622b7c011 Mon Sep 17 00:00:00 2001 From: Corina Date: Fri, 2 Oct 2026 09:27:34 +0000 Subject: [PATCH 5/5] Add A2UI version v0.9 and use buildEditMessageUri with Uri.Builder --- .../appfunctions/BaseChatAppFunctionService.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt index 11b30314..2dd4ea17 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/appfunctions/BaseChatAppFunctionService.kt @@ -141,6 +141,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { val createSurface = JSONObject().apply { + put("version", "v0.9") put( "createSurface", JSONObject().apply { @@ -157,11 +158,12 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { put("contactDisplayName", displayName) put("endpointDisplayName", "ChatApp") put("messageBody", messageBody) - put("editMessageUri", "app://chatapp/edit/$endpointValue") + put("editMessageUri", buildEditMessageUri(endpointValue, messageBody)) } val updateComponents = JSONObject().apply { + put("version", "v0.9") put( "updateComponents", JSONObject().apply { @@ -335,6 +337,19 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { return results } + private fun buildEditMessageUri( + endpointValue: String, + messageBody: String, + ): String = + Uri.Builder() + .scheme("app") + .authority("com.example.chatapp") + .appendPath("chat") + .appendPath(endpointValue) + .appendQueryParameter("draft", messageBody) + .build() + .toString() + private fun getSenderDisplayName( message: DisplayMessage, endpointId: String,