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 8988623e..2a0d5219 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 @@ -65,11 +65,9 @@ class AppFunctionInstrumentationTest { runBlocking { val sendMessageFunctionMetadata = appFunctionManager - .observeAppFunctions( + .searchAppFunctions( AppFunctionSearchSpec(packageNames = setOf(context.packageName)), ) - .first() - .flatMap { it.appFunctions } .single { it.id == ChatAppFunctionService.FUNCTION_ID_SEND_MESSAGE } val testRecipient = recipientsRepository.getAllRecipients().first() val request = @@ -103,11 +101,9 @@ class AppFunctionInstrumentationTest { runBlocking { val sendMessageFunctionMetadata = appFunctionManager - .observeAppFunctions( + .searchAppFunctions( AppFunctionSearchSpec(packageNames = setOf(context.packageName)), ) - .first() - .flatMap { it.appFunctions } .single { it.id == ChatAppFunctionService.FUNCTION_ID_SEND_MESSAGE } val testRecipient = recipientsRepository.getAllRecipients().first() val request = @@ -134,11 +130,9 @@ class AppFunctionInstrumentationTest { runBlocking { val getRecipientsFunctionMetadata = appFunctionManager - .observeAppFunctions( + .searchAppFunctions( AppFunctionSearchSpec(packageNames = setOf(context.packageName)), ) - .first() - .flatMap { it.appFunctions } .single { it.id == ChatAppFunctionService.FUNCTION_ID_SEARCH_CONTACTS } val request = diff --git a/ChatApp/gradle/libs.versions.toml b/ChatApp/gradle/libs.versions.toml index bbaa77b3..2d19a823 100644 --- a/ChatApp/gradle/libs.versions.toml +++ b/ChatApp/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "9.3.1" -appfunctions = "1.0.0-alpha10" +appfunctions = "1.0.0-beta01" kotlin = "2.4.10" coreKtx = "1.19.0" junit = "4.13.2" diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/AppFunctionsViewModel.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/AppFunctionsViewModel.kt index eb4f0ac5..c32de53f 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/AppFunctionsViewModel.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/AppFunctionsViewModel.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -66,30 +67,40 @@ class AppFunctionsViewModel private val appFunctionManager = AppFunctionManager.getInstance(application) val uiState: StateFlow = - appFunctionManager?.observeAppFunctions( - AppFunctionSearchSpec(packageNames = setOf(application.packageName)), - )?.map { packageMetadataList -> - val functions = - packageMetadataList.firstOrNull { - it.packageName == application.packageName - }?.appFunctions ?: emptyList() + appFunctionManager + ?.observeAppFunctions() + ?.map { loadAppFunctions(application.packageName) } + ?.onStart { emit(loadAppFunctions(application.packageName)) } + ?.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = AppFunctionsUiState(), + ) + ?: MutableStateFlow(AppFunctionsUiState()) + + private suspend fun loadAppFunctions(packageName: String): AppFunctionsUiState { + if (appFunctionManager == null) return AppFunctionsUiState() + val functions = + appFunctionManager.searchAppFunctions( + AppFunctionSearchSpec(packageNames = setOf(packageName)), + ) + val statesByName = + appFunctionManager + .getAppFunctionStates(functions.map { it.name }) + .associateBy { it.functionName } - val functionInfos = - functions.map { + val functionInfos = + functions + .map { AppFunctionInfo( id = it.id, - isEnabled = it.isEnabled, + isEnabled = statesByName[it.name]?.isEnabled ?: true, description = it.description, ) }.sortedBy { it.id } - AppFunctionsUiState(functions = functionInfos) - }?.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = AppFunctionsUiState(), - ) - ?: MutableStateFlow(AppFunctionsUiState()) + return AppFunctionsUiState(functions = functionInfos) + } /** * Toggles the enabled state of an app function. 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 2dd4ea17..1e53c184 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 @@ -19,14 +19,15 @@ import android.app.PendingIntent import android.content.Intent import android.net.Uri import androidx.annotation.RequiresApi -import androidx.appfunctions.AppFunction import androidx.appfunctions.AppFunctionAppUnknownException +import androidx.appfunctions.AppFunctionDeclaration import androidx.appfunctions.AppFunctionElementNotFoundException import androidx.appfunctions.AppFunctionInvalidArgumentException import androidx.appfunctions.AppFunctionService import androidx.appfunctions.AppFunctionServiceEntryPoint import androidx.appfunctions.AppFunctionStringValueConstraint import androidx.appfunctions.AppFunctionTextResource +import androidx.appfunctions.ExperimentalAppFunctionsApi import com.example.chatapp.data.CallManager import com.example.chatapp.data.DisplayMessage import com.example.chatapp.data.MessageRepository @@ -66,7 +67,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @return List of [ContactSearchResult] objects matching the query. * @throws AppFunctionInvalidArgumentException If query is empty or blank, an invalid contactType is provided, or if no matching contact or group is found. If thrown, suggest the user clarify the recipient or group name. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun searchContacts( query: String, @AppFunctionStringValueConstraint(enumValues = ["INDIVIDUAL", "GROUP", "ANY"]) @@ -119,7 +120,8 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @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) + @OptIn(ExperimentalAppFunctionsApi::class) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun fetchSendMessageDetails( endpointValue: String, messageBody: String, @@ -200,7 +202,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @throws AppFunctionElementNotFoundException If no contact or group matches endpointValue. If thrown, call "searchContacts" to find the correct ID. * @throws AppFunctionAppUnknownException If sending fails due to a repository error. If thrown, suggest the user retry later. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun sendMessage( endpointValue: String, messageBody: String, @@ -239,7 +241,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @return A [PendingIntent] that launches the call activity in the application UI. * @throws AppFunctionElementNotFoundException If no recipient exists for endpointValue. If thrown, call "searchContacts" to find the correct ID. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun makeCall(endpointValue: String): PendingIntent { val recipient = recipientsRepository.getRecipientById(endpointValue) @@ -264,7 +266,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @param chatId The unique identifier for the recipient or chat group. * @param wallpaperUri The URI of the image file to set as the chat wallpaper. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun updateChatWallpaper( chatId: String, wallpaperUri: Uri, @@ -293,7 +295,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @return List of [MessagesSearchResult] matching the query. * @throws AppFunctionInvalidArgumentException If the query is blank. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun searchMessages( query: String, endpointValue: String? = null, 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 6406856a..96c17d7d 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 @@ -18,6 +18,7 @@ package com.example.chatapp.appfunctions import androidx.appfunctions.AppFunctionResourceContainer import androidx.appfunctions.AppFunctionSerializable import androidx.appfunctions.AppFunctionTextResource +import androidx.appfunctions.ExperimentalAppFunctionsApi /** * Represents a result from a contact or group search. @@ -37,6 +38,7 @@ data class ContactSearchResult( /** * Confirmation-card content for Gemini. */ +@OptIn(ExperimentalAppFunctionsApi::class) @AppFunctionSerializable(isDescribedByKDoc = true) data class ConfirmationPreview( /** Exactly one A2UI resource (mimeType "application/a2ui+json"). */ diff --git a/agent/app/src/androidTest/java/com/example/appfunctions/agent/domain/GetAppFunctionStatesUseCaseTest.kt b/agent/app/src/androidTest/java/com/example/appfunctions/agent/domain/GetAppFunctionStatesUseCaseTest.kt index fb98fa7c..8819ed64 100644 --- a/agent/app/src/androidTest/java/com/example/appfunctions/agent/domain/GetAppFunctionStatesUseCaseTest.kt +++ b/agent/app/src/androidTest/java/com/example/appfunctions/agent/domain/GetAppFunctionStatesUseCaseTest.kt @@ -17,7 +17,7 @@ package com.example.appfunctions.agent.domain import android.content.Context import androidx.appfunctions.AppFunctionManager -import androidx.appfunctions.metadata.AppFunctionName +import androidx.appfunctions.AppFunctionName import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/data/AbstractFakeAppFunctionService.kt b/agent/app/src/main/java/com/example/appfunctions/agent/data/AbstractFakeAppFunctionService.kt index 773ed089..2d7278a7 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/data/AbstractFakeAppFunctionService.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/data/AbstractFakeAppFunctionService.kt @@ -15,7 +15,7 @@ */ package com.example.appfunctions.agent.data -import androidx.appfunctions.AppFunction +import androidx.appfunctions.AppFunctionDeclaration import androidx.appfunctions.AppFunctionSerializable import androidx.appfunctions.AppFunctionService import androidx.appfunctions.AppFunctionServiceEntryPoint @@ -34,7 +34,7 @@ abstract class AbstractFakeAppFunctionService : AppFunctionService() { * @param params The test parameters containing input data. * @return The test response containing output data. */ - @AppFunction(isDescribedByKDoc = true) + @AppFunctionDeclaration(isDescribedByKDoc = true) suspend fun fakeFunction(params: FakeParams): FakeResponse { return FakeResponse("success") } diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/data/AgentInternalTools.kt b/agent/app/src/main/java/com/example/appfunctions/agent/data/AgentInternalTools.kt index ec7189b9..7ef75704 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/data/AgentInternalTools.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/data/AgentInternalTools.kt @@ -24,11 +24,11 @@ import android.location.Geocoder import android.location.LocationManager import android.util.Base64 import androidx.annotation.RequiresApi +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.AppFunctionState import androidx.appfunctions.metadata.AppFunctionComponentsMetadata import androidx.appfunctions.metadata.AppFunctionDoubleTypeMetadata import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionObjectTypeMetadata import androidx.appfunctions.metadata.AppFunctionPackageMetadata import androidx.appfunctions.metadata.AppFunctionParameterMetadata @@ -390,7 +390,6 @@ class AgentInternalTools packageMetadata = AppFunctionPackageMetadata( packageName = INTERNAL_TOOL_PACKAGE, - appFunctions = listOf(), components = AppFunctionComponentsMetadata(), ), ) @@ -418,7 +417,6 @@ class AgentInternalTools packageMetadata = AppFunctionPackageMetadata( packageName = INTERNAL_TOOL_PACKAGE, - appFunctions = listOf(), components = AppFunctionComponentsMetadata(), ), ) @@ -452,7 +450,6 @@ class AgentInternalTools packageMetadata = AppFunctionPackageMetadata( packageName = INTERNAL_TOOL_PACKAGE, - appFunctions = listOf(), components = AppFunctionComponentsMetadata(), ), ) diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/GetAppFunctionStatesUseCase.kt b/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/GetAppFunctionStatesUseCase.kt index 7959866e..1a8e5959 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/GetAppFunctionStatesUseCase.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/GetAppFunctionStatesUseCase.kt @@ -16,8 +16,8 @@ package com.example.appfunctions.agent.domain.appfunction import androidx.appfunctions.AppFunctionManager +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.AppFunctionState -import androidx.appfunctions.metadata.AppFunctionName import javax.inject.Inject /** Use case to get [AppFunctionState] with given list of [AppFunctionName]. */ diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionItem.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionItem.kt index 384c856e..caded5d2 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionItem.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionItem.kt @@ -15,10 +15,10 @@ */ package com.example.appfunctions.agent.ui.screens.debugging +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.metadata.AppFunctionComponentsMetadata import androidx.appfunctions.metadata.AppFunctionDataTypeMetadata import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionPackageMetadata import androidx.appfunctions.metadata.AppFunctionParameterMetadata import androidx.appfunctions.metadata.AppFunctionResponseMetadata @@ -328,7 +328,7 @@ fun AppFunctionItemPreview() { packageMetadata = AppFunctionPackageMetadata( packageName = "com.example.test", - appFunctions = listOf(), + components = AppFunctionComponentsMetadata(), ), ) @@ -365,7 +365,7 @@ fun AppFunctionItem_NoParams_Preview() { packageMetadata = AppFunctionPackageMetadata( packageName = "com.example.test", - appFunctions = listOf(), + components = AppFunctionComponentsMetadata(), ), ) diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingUiState.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingUiState.kt index 89205baf..5c595739 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingUiState.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingUiState.kt @@ -15,8 +15,8 @@ */ package com.example.appfunctions.agent.ui.screens.debugging +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import com.example.appfunctions.agent.domain.appfunction.AppInfo import com.example.appfunctions.agent.domain.appfunction.ExecuteAppFunctionResult import com.example.appfunctions.agent.domain.troubleshoot.TroubleshootReport diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModel.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModel.kt index 69e3e527..f929cd50 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModel.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModel.kt @@ -18,9 +18,9 @@ package com.example.appfunctions.agent.ui.screens.debugging import android.app.PendingIntent import android.content.res.Resources import android.util.Log +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.AppFunctionState import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionPackageMetadata import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/FunctionsFoundContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/FunctionsFoundContent.kt index 8c967daa..65ae21ec 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/FunctionsFoundContent.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/FunctionsFoundContent.kt @@ -17,8 +17,8 @@ package com.example.appfunctions.agent.ui.screens.debugging import android.app.PendingIntent import android.widget.Toast +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionPackageMetadata import androidx.appfunctions.metadata.AppFunctionResponseMetadata import androidx.appfunctions.metadata.AppFunctionStringTypeMetadata @@ -320,7 +320,6 @@ fun AppFunctionErrorItemPreview() { packageMetadata = AppFunctionPackageMetadata( packageName = "com.example.test", - appFunctions = listOf(), components = androidx.appfunctions.metadata.AppFunctionComponentsMetadata(), ), ) @@ -351,7 +350,6 @@ fun FunctionsFoundContentPreview() { packageMetadata = AppFunctionPackageMetadata( packageName = "com.example.test", - appFunctions = listOf(), components = androidx.appfunctions.metadata.AppFunctionComponentsMetadata(), ), ) @@ -368,7 +366,6 @@ fun FunctionsFoundContentPreview() { packageMetadata = AppFunctionPackageMetadata( packageName = "com.example.test", - appFunctions = listOf(), components = androidx.appfunctions.metadata.AppFunctionComponentsMetadata(), ), ) diff --git a/agent/app/src/test/java/com/example/appfunctions/agent/domain/AgentOrchestratorTest.kt b/agent/app/src/test/java/com/example/appfunctions/agent/domain/AgentOrchestratorTest.kt index f37846e7..8c26dbd8 100644 --- a/agent/app/src/test/java/com/example/appfunctions/agent/domain/AgentOrchestratorTest.kt +++ b/agent/app/src/test/java/com/example/appfunctions/agent/domain/AgentOrchestratorTest.kt @@ -17,9 +17,9 @@ package com.example.appfunctions.agent.domain import android.content.Intent import androidx.appfunctions.AppFunctionData +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.AppFunctionState import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionPackageMetadata import com.example.appfunctions.agent.data.AgentInternalTools import com.example.appfunctions.agent.data.LlmModel @@ -377,13 +377,11 @@ class AgentOrchestratorTest { private fun createMockTool( packageName: String, id: String, - isEnabled: Boolean = true, ): AppFunctionMetadata { val tool = mockk(relaxed = true) every { tool.packageName } returns packageName every { tool.id } returns id every { tool.name } returns AppFunctionName(packageName, id) - every { tool.isEnabled } returns isEnabled return tool } diff --git a/agent/app/src/test/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModelTest.kt b/agent/app/src/test/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModelTest.kt index b07495fd..fb64fb53 100644 --- a/agent/app/src/test/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModelTest.kt +++ b/agent/app/src/test/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingViewModelTest.kt @@ -21,9 +21,9 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.graphics.Color import android.graphics.drawable.ColorDrawable +import androidx.appfunctions.AppFunctionName import androidx.appfunctions.AppFunctionState import androidx.appfunctions.metadata.AppFunctionMetadata -import androidx.appfunctions.metadata.AppFunctionName import androidx.appfunctions.metadata.AppFunctionPackageMetadata import androidx.test.core.app.ApplicationProvider import com.example.appfunctions.agent.R diff --git a/agent/gradle/libs.versions.toml b/agent/gradle/libs.versions.toml index 27f61cb9..72769f1c 100644 --- a/agent/gradle/libs.versions.toml +++ b/agent/gradle/libs.versions.toml @@ -20,7 +20,7 @@ mockk = "1.14.11" ksp = "2.3.10" hilt = "2.60.1" androidxHiltNavigationCompose = "1.4.0" -appfunctions = "1.0.0-alpha11" +appfunctions = "1.0.0-beta01" datastore = "1.2.1" screenshot = "0.0.1-alpha15" coil = "2.7.0"