diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9416c63 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_function_naming_ignore_when_annotated_with = Composable 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 74e7e8e..4678127 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 @@ -24,7 +24,6 @@ import androidx.appfunctions.ExecuteAppFunctionRequest import androidx.appfunctions.ExecuteAppFunctionResponse import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.example.chatapp.appfunctions.ChatAppFunctionService import com.example.chatapp.data.MessageRepository import com.example.chatapp.data.RecipientsRepository import com.google.common.truth.Truth.assertThat diff --git a/ChatApp/app/src/main/kotlin/com/example/chatapp/uicomponents/ChatScreen.kt b/ChatApp/app/src/main/kotlin/com/example/chatapp/uicomponents/ChatScreen.kt index 388dd89..13f1b9d 100644 --- a/ChatApp/app/src/main/kotlin/com/example/chatapp/uicomponents/ChatScreen.kt +++ b/ChatApp/app/src/main/kotlin/com/example/chatapp/uicomponents/ChatScreen.kt @@ -269,14 +269,16 @@ fun MessageBubble( } } if (message.content.isNotEmpty()) { - val linkColor = if (message.isInbound) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.inversePrimary - } - val annotatedText = remember(message.content, linkColor) { - linkifyString(message.content, linkColor = linkColor) - } + val linkColor = + if (message.isInbound) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.inversePrimary + } + val annotatedText = + remember(message.content, linkColor) { + linkifyString(message.content, linkColor = linkColor) + } Text( modifier = Modifier.padding(16.dp), text = annotatedText, diff --git a/ChatApp/build.gradle.kts b/ChatApp/build.gradle.kts index d0abb64..8a1d167 100644 --- a/ChatApp/build.gradle.kts +++ b/ChatApp/build.gradle.kts @@ -44,12 +44,14 @@ fun SpotlessExtension.spotlessConfiguration() { target("src/**/*.kt") targetExclude("**/build/**/*.kt") ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${rootProject.projectDir.parentFile}/.editorconfig") licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } kotlinGradle { target("*.kts") targetExclude("**/build/**/*.kts") ktlint(libs.versions.ktlint.get()) + .setEditorConfigPath("${rootProject.projectDir.parentFile}/.editorconfig") licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\\\/ ]\\\\*).*$)") } } diff --git a/ChatApp/shared/build.gradle.kts b/ChatApp/shared/build.gradle.kts index b9aba68..5a720e6 100644 --- a/ChatApp/shared/build.gradle.kts +++ b/ChatApp/shared/build.gradle.kts @@ -48,5 +48,4 @@ dependencies { // App functions implementation(libs.androidx.appfunctions) ksp(libs.androidx.appfunctions.compiler) - } diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/BaseChatApplication.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/BaseChatApplication.kt index b051a26..4d53914 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/BaseChatApplication.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/BaseChatApplication.kt @@ -16,4 +16,5 @@ package com.example.chatapp import android.app.Application + abstract class BaseChatApplication : Application() 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 caae605..19cfa52 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,19 +19,19 @@ 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.AppFunctionElementNotFoundException import androidx.appfunctions.AppFunctionInvalidArgumentException import androidx.appfunctions.AppFunctionService import androidx.appfunctions.AppFunctionServiceEntryPoint import androidx.appfunctions.AppFunctionStringValueConstraint -import androidx.appfunctions.AppFunction import com.example.chatapp.data.CallManager import com.example.chatapp.data.MessageRepository import com.example.chatapp.data.RecipientsRepository import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import kotlinx.coroutines.CancellationException +import javax.inject.Inject /** * Service entry point for chat-related AppFunctions such as searching contacts, sending messages, and making calls. @@ -44,7 +44,9 @@ import kotlinx.coroutines.CancellationException ) abstract class BaseChatAppFunctionService : AppFunctionService() { @Inject lateinit var messageRepository: MessageRepository + @Inject lateinit var recipientsRepository: RecipientsRepository + @Inject lateinit var callManager: CallManager /** @@ -153,9 +155,7 @@ abstract class BaseChatAppFunctionService : AppFunctionService() { * @throws AppFunctionElementNotFoundException If no recipient exists for endpointValue. If thrown, call "searchContacts" to find the correct ID. */ @AppFunction(isDescribedByKDoc = true) - suspend fun makeCall( - endpointValue: String, - ): PendingIntent { + suspend fun makeCall(endpointValue: String): PendingIntent { val recipient = recipientsRepository.getRecipientById(endpointValue) ?: throw AppFunctionElementNotFoundException( 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 81d0354..51ccd17 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 @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.example.chatapp.appfunctions import androidx.appfunctions.AppFunctionSerializable - /** * Represents a result from a contact or group search. */ @@ -67,4 +65,4 @@ data class ChatGroup( val name: String, /** List of members belonging to the group. */ val recipients: List, -) \ No newline at end of file +) diff --git a/ChatApp/shared/src/main/kotlin/com/example/chatapp/util/Linkify.kt b/ChatApp/shared/src/main/kotlin/com/example/chatapp/util/Linkify.kt index f0a8f93..453ff4f 100644 --- a/ChatApp/shared/src/main/kotlin/com/example/chatapp/util/Linkify.kt +++ b/ChatApp/shared/src/main/kotlin/com/example/chatapp/util/Linkify.kt @@ -24,7 +24,10 @@ import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration -fun linkifyString(text: String, linkColor: Color? = null): AnnotatedString { +fun linkifyString( + text: String, + linkColor: Color? = null, +): AnnotatedString { val matcher = Patterns.WEB_URL.matcher(text) var lastIndex = 0 return buildAnnotatedString { @@ -35,22 +38,24 @@ fun linkifyString(text: String, linkColor: Color? = null): AnnotatedString { append(text.substring(lastIndex, start)) - val uriStr = if (url.startsWith("http://", ignoreCase = true) || url.startsWith("https://", ignoreCase = true)) { - url - } else { - "https://$url" - } - - val linkStyle = SpanStyle( - color = linkColor ?: Color.Unspecified, - textDecoration = TextDecoration.Underline - ) + val uriStr = + if (url.startsWith("http://", ignoreCase = true) || url.startsWith("https://", ignoreCase = true)) { + url + } else { + "https://$url" + } + + val linkStyle = + SpanStyle( + color = linkColor ?: Color.Unspecified, + textDecoration = TextDecoration.Underline, + ) pushLink( LinkAnnotation.Url( url = uriStr, - styles = TextLinkStyles(style = linkStyle) - ) + styles = TextLinkStyles(style = linkStyle), + ), ) append(url) pop() diff --git a/ChatApp/wear/build.gradle.kts b/ChatApp/wear/build.gradle.kts index fe02a1e..23bbd5b 100644 --- a/ChatApp/wear/build.gradle.kts +++ b/ChatApp/wear/build.gradle.kts @@ -67,7 +67,6 @@ dependencies { // App functions implementation(libs.androidx.appfunctions) ksp(libs.androidx.appfunctions.compiler) - } // AppFunctions ksp option diff --git a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt index c710615..5b21082 100644 --- a/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt +++ b/ChatApp/wear/src/main/kotlin/com/example/chatapp/wear/uicomponents/WearChatScreen.kt @@ -70,9 +70,10 @@ fun WearChatScreen( title = { Text(text = if (message.isInbound) message.senderName ?: "Sender" else "Me") }, ) { val linkColor = MaterialTheme.colorScheme.primary - val annotatedText = remember(message.content, linkColor) { - linkifyString(text = message.content, linkColor = linkColor) - } + val annotatedText = + remember(message.content, linkColor) { + linkifyString(text = message.content, linkColor = linkColor) + } Text(text = annotatedText) } } diff --git a/README.md b/README.md index 03aecf9..4f31f1c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Follow these steps to explore and run the samples: We'd love to accept your patches and contributions! - **Contributor License Agreement (CLA)**: All contributors must sign the [Google CLA](https://cla.developers.google.com/). -- **Code Style**: We use [Spotless](https://github.com/diffplug/spotless) to maintain consistent formatting. Run `./gradlew spotlessApply` before committing. +- **Code Style**: We use [Spotless](https://github.com/diffplug/spotless) to maintain consistent formatting. Because this repository contains separate Gradle projects for each sample, run `./agent/gradlew -p agent spotlessApply && ./ChatApp/gradlew -p ChatApp spotlessApply` from the repository root (or `./gradlew spotlessApply` inside a specific sample directory) before committing. - **Testing**: Ensure all new functionality is covered by unit or instrumentation tests. - **Reviews**: All submissions are reviewed via GitHub Pull Requests. diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/data/BuiltInAppFunctions.kt b/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt similarity index 98% rename from agent/app/src/main/java/com/example/appfunctions/agent/data/BuiltInAppFunctions.kt rename to agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt index 693f7bb..021bc8d 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/data/BuiltInAppFunctions.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseBuiltInAppFunctionService.kt @@ -50,9 +50,7 @@ abstract class BaseBuiltInAppFunctionService : AppFunctionService() { * @return The latitude and longitude coordinates of the address, or null if geocoding fails. */ @AppFunction(isDescribedByKDoc = true) - suspend fun geocodeAddress( - address: String, - ): LatLng? { + suspend fun geocodeAddress(address: String): LatLng? { if (!Geocoder.isPresent()) { return null } diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/data/FakeAppFunctions.kt b/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseFakeAppFunctionService.kt similarity index 96% rename from agent/app/src/main/java/com/example/appfunctions/agent/data/FakeAppFunctions.kt rename to agent/app/src/main/java/com/example/appfunctions/agent/data/BaseFakeAppFunctionService.kt index 1f82650..2048ee9 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/data/FakeAppFunctions.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/data/BaseFakeAppFunctionService.kt @@ -37,9 +37,7 @@ abstract class BaseFakeAppFunctionService : AppFunctionService() { * @return The test response containing output data. */ @AppFunction(isDescribedByKDoc = true) - suspend fun fakeFunction( - params: FakeParams, - ): FakeResponse { + suspend fun fakeFunction(params: FakeParams): FakeResponse { return FakeResponse("success") } diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/domain/AgentOrchestrator.kt b/agent/app/src/main/java/com/example/appfunctions/agent/domain/AgentOrchestrator.kt index 6b47c36..8ba18b0 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/domain/AgentOrchestrator.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/domain/AgentOrchestrator.kt @@ -17,7 +17,6 @@ package com.example.appfunctions.agent.domain import android.app.PendingIntent import android.util.Log -import androidx.appfunctions.AppFunctionException import androidx.appfunctions.metadata.AppFunctionMetadata import com.example.appfunctions.agent.data.LlmProviderName import com.example.appfunctions.agent.data.SettingsRepository @@ -38,7 +37,6 @@ import com.example.appfunctions.agent.domain.chat.UpdateMessageUseCase import com.example.appfunctions.agent.domain.chat.UpdateThreadParams import com.example.appfunctions.agent.domain.chat.UpdateThreadUseCase import com.example.appfunctions.agent.domain.pendingintent.SavePendingIntentUseCase -import java.util.UUID import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope @@ -50,6 +48,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatter.kt b/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatter.kt index c3b381a..fc42248 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatter.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatter.kt @@ -22,7 +22,10 @@ object AppFunctionExceptionFormatter { /** * Formats the given [exception] including its class name and message. */ - fun format(exception: AppFunctionException, functionId: String? = null): String { + fun format( + exception: AppFunctionException, + functionId: String? = null, + ): String { val className = exception.javaClass.simpleName val message = exception.errorMessage ?: exception.message ?: "No error message provided" val prefix = if (functionId != null) "Tool execution failed for $functionId: " else "" 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 146d1f6..ca95ccb 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 @@ -65,7 +65,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.appfunctions.AppFunctionException import com.example.appfunctions.agent.R import com.example.appfunctions.agent.domain.appfunction.AppFunctionExceptionFormatter import com.example.appfunctions.agent.domain.appfunction.ExecuteAppFunctionResult 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 4dbf97d..d424089 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 @@ -282,17 +282,18 @@ class AgentOrchestratorTest { coEvery { llmProvider.generateResponse(null, any(), any(), any(), any()) - } returns LlmResponse.Success( - "interaction_1", - listOf( - LlmResponsePart.ToolCall( - packageName = "com.example.calendar", - functionId = "create_event", - arguments = emptyMap(), - callId = "call_1", - ) + } returns + LlmResponse.Success( + "interaction_1", + listOf( + LlmResponsePart.ToolCall( + packageName = "com.example.calendar", + functionId = "create_event", + arguments = emptyMap(), + callId = "call_1", + ), + ), ) - ) val expectedErrorOutput = "Tool execution failed for create_event: Error: AppFunctionPermissionRequiredException - Calendar permission required" @@ -305,17 +306,18 @@ class AgentOrchestratorTest { coVerify { llmProvider.generateResponse( previousInteractionId = eq("interaction_1"), - input = eq( - LlmInput.ToolResponse( - listOf( - ToolOutput( - functionId = "create_event", - callId = "call_1", - result = expectedErrorOutput, - ) - ) - ) - ), + input = + eq( + LlmInput.ToolResponse( + listOf( + ToolOutput( + functionId = "create_event", + callId = "call_1", + result = expectedErrorOutput, + ), + ), + ), + ), tools = listOf(tool1), apiKey = "dummy_key", modelName = any(), diff --git a/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatterTest.kt b/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatterTest.kt index b60f919..906a3dd 100644 --- a/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatterTest.kt +++ b/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/AppFunctionExceptionFormatterTest.kt @@ -86,7 +86,8 @@ class AppFunctionExceptionFormatterTest { "com.example.chatapp.appfunctions.BaseChatAppFunctionService#send", ) assertEquals( - "Tool execution failed for com.example.chatapp.appfunctions.BaseChatAppFunctionService#send: Error: AppFunctionInvalidArgumentException - Message body cannot be empty", + "Tool execution failed for com.example.chatapp.appfunctions.BaseChatAppFunctionService#send: " + + "Error: AppFunctionInvalidArgumentException - Message body cannot be empty", formatted, ) } diff --git a/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/ExecuteAppFunctionUseCaseTest.kt b/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/ExecuteAppFunctionUseCaseTest.kt index 7d4db46..9c660ff 100644 --- a/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/ExecuteAppFunctionUseCaseTest.kt +++ b/agent/app/src/test/java/com/example/appfunctions/agent/domain/appfunction/ExecuteAppFunctionUseCaseTest.kt @@ -38,38 +38,39 @@ class ExecuteAppFunctionUseCaseTest { private val convertAppFunctionDataToJsonUseCase = mockk() private val useCase = ExecuteAppFunctionUseCase(appFunctionManager, convertAppFunctionDataToJsonUseCase) - @Test - fun `invoke returns Error with original AppFunctionException when response is Error`() = runTest { - val function = mockk(relaxed = true) - every { function.packageName } returns "com.example.app" - every { function.id } returns "test_function" - val parameters = mockk() + fun `invoke returns Error with original AppFunctionException when response is Error`() = + runTest { + val function = mockk(relaxed = true) + every { function.packageName } returns "com.example.app" + every { function.id } returns "test_function" + val parameters = mockk() - val appException = AppFunctionPermissionRequiredException("Permission needed") - coEvery { appFunctionManager.executeAppFunction(any()) } returns ExecuteAppFunctionResponse.Error(appException) + val appException = AppFunctionPermissionRequiredException("Permission needed") + coEvery { appFunctionManager.executeAppFunction(any()) } returns ExecuteAppFunctionResponse.Error(appException) - val result = useCase(function, parameters) + val result = useCase(function, parameters) - assertTrue(result is ExecuteAppFunctionResult.Error) - val errorResult = result as ExecuteAppFunctionResult.Error - assertEquals(appException, errorResult.exception) - } + assertTrue(result is ExecuteAppFunctionResult.Error) + val errorResult = result as ExecuteAppFunctionResult.Error + assertEquals(appException, errorResult.exception) + } @Test - fun `invoke returns Error when executeAppFunction throws exception`() = runTest { - val function = mockk(relaxed = true) - every { function.packageName } returns "com.example.app" - every { function.id } returns "test_function" - val parameters = mockk() + fun `invoke returns Error when executeAppFunction throws exception`() = + runTest { + val function = mockk(relaxed = true) + every { function.packageName } returns "com.example.app" + every { function.id } returns "test_function" + val parameters = mockk() - val runtimeException = RuntimeException("Unexpected crash") - coEvery { appFunctionManager.executeAppFunction(any()) } throws runtimeException + val runtimeException = RuntimeException("Unexpected crash") + coEvery { appFunctionManager.executeAppFunction(any()) } throws runtimeException - val result = useCase(function, parameters) + val result = useCase(function, parameters) - assertTrue(result is ExecuteAppFunctionResult.Error) - val errorResult = result as ExecuteAppFunctionResult.Error - assertEquals(runtimeException, errorResult.exception) - } + assertTrue(result is ExecuteAppFunctionResult.Error) + val errorResult = result as ExecuteAppFunctionResult.Error + assertEquals(runtimeException, errorResult.exception) + } } diff --git a/agent/build.gradle.kts b/agent/build.gradle.kts index 78eca1b..b1d5f31 100644 --- a/agent/build.gradle.kts +++ b/agent/build.gradle.kts @@ -43,14 +43,14 @@ fun SpotlessExtension.spotlessConfiguration() { target("src/**/*.kt") targetExclude("**/build/**/*.kt") ktlint(libs.versions.ktlint.get()) - .setEditorConfigPath("${project.rootDir}/.editorconfig") + .setEditorConfigPath("${rootProject.projectDir.parentFile}/.editorconfig") licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } kotlinGradle { target("*.kts") targetExclude("**/build/**/*.kts") ktlint(libs.versions.ktlint.get()) - .setEditorConfigPath("${project.rootDir}/.editorconfig") + .setEditorConfigPath("${rootProject.projectDir.parentFile}/.editorconfig") licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\\\/ ]\\\\*).*$)") } format("xml") {