Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions agent/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Expand All @@ -39,6 +41,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -50,6 +53,14 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="appfunctions-agent" android:host="chat" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ASSIST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VOICE_ASSIST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<activity
Expand All @@ -70,6 +81,34 @@
</provider>


<service
android:name="com.example.appfunctions.agent.voice.AgentVoiceInteractionService"
android:label="@string/app_name"
android:permission="android.permission.BIND_VOICE_INTERACTION"
android:exported="true">
<meta-data
android:name="android.voice_interaction"
android:resource="@xml/voice_interaction_service" />
<intent-filter>
<action android:name="android.service.voice.VoiceInteractionService" />
</intent-filter>
</service>

<service
android:name="com.example.appfunctions.agent.voice.AgentVoiceInteractionSessionService"
android:permission="android.permission.BIND_VOICE_INTERACTION"
android:exported="true" />

<service
android:name="com.example.appfunctions.agent.voice.AgentRecognitionService"
android:label="@string/app_name"
android:permission="android.permission.BIND_RECOGNITION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.speech.RecognitionService" />
</intent-filter>
</service>

<service
android:name="com.example.appfunctions.agent.data.FakeAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
package com.example.appfunctions.agent

import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -36,13 +39,17 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.util.Consumer
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand Down Expand Up @@ -99,6 +106,8 @@ class MainActivity : ComponentActivity() {
const val ROUTE_AGENT_DEMO = "agent_demo?threadId={threadId}"
const val ARG_THREAD_ID = "threadId"
const val DEEPLINK_URI_PATTERN = "appfunctions-agent://chat?threadId={threadId}"
const val ACTION_OPEN_CHAT = "com.example.appfunctions.agent.action.OPEN_CHAT"
const val EXTRA_OPEN_THREAD_ID = "EXTRA_OPEN_THREAD_ID"
const val EXTRA_INSTRUMENTATION_ERROR = "EXTRA_INSTRUMENTATION_ERROR"
}
}
Expand All @@ -115,6 +124,21 @@ fun MainScreen(
) {
val navController = rememberNavController()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val activity = LocalActivity.current as? ComponentActivity

LaunchedEffect(activity, navController) {
navigateToChatIfRequested(navController, activity?.intent)
}

DisposableEffect(activity, navController) {
val listener =
Consumer<Intent> { newIntent ->
activity?.intent = newIntent
navigateToChatIfRequested(navController, newIntent)
}
activity?.addOnNewIntentListener(listener)
onDispose { activity?.removeOnNewIntentListener(listener) }
}

val items = listOf("debugging", "agent_demo", "settings")
val icons = listOf(Icons.Default.Terminal, Icons.Default.PlayCircle, Icons.Default.Settings)
Expand Down Expand Up @@ -215,3 +239,44 @@ fun MainScreen(
}
}
}

private fun navigateToChatIfRequested(
navController: NavHostController,
intent: Intent?,
) {
if (intent == null) return
val isOpenChatIntent =
intent.action == MainActivity.ACTION_OPEN_CHAT ||
intent.hasExtra(MainActivity.EXTRA_OPEN_THREAD_ID)
val deepLinkUri =
intent.data?.takeIf { it.scheme == "appfunctions-agent" && it.host == "chat" }
if (!isOpenChatIntent && deepLinkUri == null) return

val threadId =
intent.getStringExtra(MainActivity.EXTRA_OPEN_THREAD_ID)
?: deepLinkUri?.getQueryParameter(MainActivity.ARG_THREAD_ID)

if (intent.action == MainActivity.ACTION_OPEN_CHAT) {
intent.action = null
}
intent.removeExtra(MainActivity.EXTRA_OPEN_THREAD_ID)

val route =
if (!threadId.isNullOrEmpty()) {
"agent_demo?threadId=${Uri.encode(threadId)}"
} else {
"agent_demo"
}

navController.navigate(route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
}
if (!threadId.isNullOrEmpty()) {
navController.currentBackStackEntry
?.savedStateHandle
?.set(MainActivity.ARG_THREAD_ID, threadId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class DataStoreSettingsRepository
val GEMINI_API_KEY = stringPreferencesKey("gemini_api_key")
val SELECTED_PROVIDER = stringPreferencesKey("selected_provider")
val SERVICE_TIER = stringPreferencesKey("service_tier")
val GEMINI_LIVE_VOICE = stringPreferencesKey("gemini_live_voice")
val PINNED_APPS = stringSetPreferencesKey("pinned_apps")
val DISCONNECTED_APPS = stringSetPreferencesKey("disconnected_apps")
}
Expand Down Expand Up @@ -65,6 +66,11 @@ class DataStoreSettingsRepository
} ?: ServiceTier.PRIORITY
}

override val geminiLiveVoice: Flow<GeminiLiveVoice> =
dataStore.data.map { preferences ->
GeminiLiveVoice.fromStoredValue(preferences[PreferencesKeys.GEMINI_LIVE_VOICE])
}

override val pinnedApps: Flow<Set<String>> =
dataStore.data.map { preferences -> preferences[PreferencesKeys.PINNED_APPS] ?: emptySet() }

Expand All @@ -87,6 +93,12 @@ class DataStoreSettingsRepository
dataStore.edit { preferences -> preferences[PreferencesKeys.SERVICE_TIER] = tier.name }
}

override suspend fun setGeminiLiveVoice(voice: GeminiLiveVoice) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.GEMINI_LIVE_VOICE] = voice.name
}
}

override suspend fun setAppPinned(
packageName: String,
pinned: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.appfunctions.agent.data

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

/** Converts a [JsonElement] recursively into standard Kotlin primitive, [Map], or [List] values. */
internal fun JsonElement.toPrimitive(): Any? {
return when (this) {
JsonNull -> null
is JsonPrimitive -> {
if (this.isString) return this.content
return this.content.toBooleanStrictOrNull()
?: this.content.toLongOrNull()
?: this.content.toDoubleOrNull()
}
is JsonObject -> this.mapValues { it.value.toPrimitive() }
is JsonArray -> this.map { it.toPrimitive() }
}
}

/** Converts a nullable JSON arguments object into a Kotlin arguments map. */
internal fun JsonObject?.toArgumentsMap(): Map<String, Any?> {
if (this == null) return emptyMap()
return mapValues { (_, value) -> value.toPrimitive() }
}
Loading
Loading