diff --git a/agent/app/src/main/AndroidManifest.xml b/agent/app/src/main/AndroidManifest.xml index 7fa52ed..642e51a 100644 --- a/agent/app/src/main/AndroidManifest.xml +++ b/agent/app/src/main/AndroidManifest.xml @@ -25,9 +25,12 @@ + + + diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt index 6b4a6a8..5dfe32f 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/MainActivity.kt @@ -21,6 +21,9 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -30,16 +33,16 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Terminal import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination @@ -50,6 +53,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import com.example.appfunctions.agent.ui.layout.AdaptiveMainNavigation import com.example.appfunctions.agent.ui.screens.agentdemo.AgentDemoScreen import com.example.appfunctions.agent.ui.screens.agentdemo.ConnectedAppsScreen import com.example.appfunctions.agent.ui.screens.agentdemo.SettingsScreen @@ -155,38 +159,16 @@ fun MainScreen( } } - Scaffold( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - bottomBar = { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - items.forEachIndexed { index, screen -> - NavigationBarItem( - icon = { Icon(icons[index], contentDescription = labels[index]) }, - label = { Text(labels[index]) }, - selected = - currentDestination?.hierarchy?.any { - it.route?.startsWith(screen) == true - } == true, - onClick = { - navController.navigate(screen) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - ) - } - } - }, - ) { innerPadding -> + AdaptiveMainNavigation( + navController = navController, + items = items, + icons = icons, + labels = labels, + ) { contentModifier -> NavHost( navController = navController, startDestination = "debugging", - modifier = Modifier.padding(innerPadding).consumeWindowInsets(innerPadding), + modifier = contentModifier, ) { composable("debugging") { DebuggingScreen() } composable( diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/domain/troubleshoot/CheckMainlineVersionUseCase.kt b/agent/app/src/main/java/com/example/appfunctions/agent/domain/troubleshoot/CheckMainlineVersionUseCase.kt index abad414..cc9337c 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/domain/troubleshoot/CheckMainlineVersionUseCase.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/domain/troubleshoot/CheckMainlineVersionUseCase.kt @@ -32,17 +32,21 @@ class CheckMainlineVersionUseCase */ operator fun invoke(): Boolean { val pm = context.packageManager - return try { - val packageInfo = - pm.getPackageInfo( - APPSEARCH_MODULE_NAME, - PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX.toLong()), - ) - val versionCode = packageInfo.longVersionCode - versionCode > REQUIRED_VERSION - } catch (e: PackageManager.NameNotFoundException) { - false + for (moduleName in listOf(APPSEARCH_MODULE_NAME, "com.android.appsearch")) { + try { + val packageInfo = + pm.getPackageInfo( + moduleName, + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX.toLong()), + ) + val versionCode = packageInfo.longVersionCode + if (versionCode > REQUIRED_VERSION) return true + } catch (e: PackageManager.NameNotFoundException) { + // Try next package or fallback + } } + // Fallback to true if running on API Level >= 36 + return android.os.Build.VERSION.SDK_INT >= 36 } companion object { diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/components/TvSurfaceTextField.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/components/TvSurfaceTextField.kt new file mode 100644 index 0000000..a27015f --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/components/TvSurfaceTextField.kt @@ -0,0 +1,220 @@ +/* + * 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.ui.components + +import kotlin.OptIn +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.text.TextRange +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.unit.dp +import com.example.appfunctions.agent.R + +/** + * Android TV Surface Paradigm Text Field. + * + * Unfocused / Idle state: Renders a focusable Surface container that responds to D-Pad navigation. + * High contrast focus stroke border and scale boost when focused. + * Pressing D-Pad Center (or click) enters editing mode and focuses the underlying OutlinedTextField. + * Pressing Back or Enter/Done exits editing mode, hides the keyboard, and restores D-Pad navigation. + */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun TvSurfaceTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + label: String? = null, + singleLine: Boolean = true, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + shape: Shape = CircleShape, +) { + var isEditing by remember { mutableStateOf(false) } + var wasEditing by remember { mutableStateOf(false) } + val keyboardController = LocalSoftwareKeyboardController.current + val textFieldFocusRequester = remember { FocusRequester() } + val surfaceFocusRequester = remember { FocusRequester() } + + val isImeVisible = WindowInsets.isImeVisible + var keyboardHasShown by remember(isEditing) { mutableStateOf(false) } + + fun stopEditing() { + isEditing = false + keyboardController?.hide() + } + + BackHandler(enabled = isEditing) { + stopEditing() + } + + LaunchedEffect(isEditing) { + if (isEditing) { + wasEditing = true + textFieldFocusRequester.requestFocus() + } else if (wasEditing) { + surfaceFocusRequester.requestFocus() + } + } + + LaunchedEffect(isImeVisible) { + if (isImeVisible) { + keyboardHasShown = true + } else if (keyboardHasShown && isEditing) { + stopEditing() + } + } + + if (isEditing) { + var tfValue by remember { + mutableStateOf( + androidx.compose.ui.text.input.TextFieldValue( + text = value, + selection = androidx.compose.ui.text.TextRange(0, value.length), + ) + ) + } + LaunchedEffect(value) { + if (tfValue.text != value) { + tfValue = tfValue.copy(text = value, selection = TextRange(value.length)) + } + } + var tfFocused by remember { mutableStateOf(false) } + OutlinedTextField( + value = tfValue, + onValueChange = { newTfValue -> + tfValue = newTfValue + onValueChange(newTfValue.text) + }, + singleLine = singleLine, + label = label?.let { { Text(it) } }, + placeholder = { Text(placeholder) }, + trailingIcon = trailingIcon, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions( + onDone = { stopEditing() }, + onSearch = { stopEditing() }, + ), + shape = shape, + colors = OutlinedTextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surfaceBright, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceBright, + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + ), + modifier = modifier + .defaultMinSize(minHeight = 52.dp) + .focusRequester(textFieldFocusRequester) + .onFocusChanged { focusState -> + if (tfFocused && !focusState.isFocused) { + stopEditing() + } + tfFocused = focusState.isFocused + } + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.key == Key.Back) { + if (keyEvent.type == KeyEventType.KeyDown || keyEvent.type == KeyEventType.KeyUp) { + stopEditing() + } + true + } else false + } + ) + } else { + var isFocused by remember { mutableStateOf(false) } + val scale by animateFloatAsState(if (isFocused) 1.02f else 1.0f, label = "textFieldScale") + + Surface( + onClick = { isEditing = true }, + modifier = modifier + .defaultMinSize(minHeight = 52.dp) + .scale(scale) + .focusRequester(surfaceFocusRequester) + .onFocusChanged { isFocused = it.isFocused }, + shape = shape, + color = if (isFocused) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceBright, + border = if (isFocused) { + BorderStroke(2.5.dp, MaterialTheme.colorScheme.primary) + } else { + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + }, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + if (label != null) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = if (isFocused) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.primary, + ) + } + Text( + text = value.ifEmpty { placeholder }, + color = when { + isFocused -> MaterialTheme.colorScheme.onPrimaryContainer + value.isNotEmpty() -> MaterialTheme.colorScheme.onSurface + else -> MaterialTheme.colorScheme.onSurfaceVariant + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + trailingIcon?.invoke() + } + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/AgentDemoScreenLayout.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/AgentDemoScreenLayout.kt new file mode 100644 index 0000000..e222594 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/AgentDemoScreenLayout.kt @@ -0,0 +1,35 @@ +/* + * 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.ui.contracts + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiEvent +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiState + +/** + * Mandatory interface contract for Agent Demo screen layouts. + * Both Mobile and TV implementations MUST conform to this contract. + */ +interface AgentDemoScreenLayout { + @Composable + fun Content( + uiState: AgentUiState, + onEvent: (AgentUiEvent) -> Unit, + initialSidePanelVisible: Boolean, + modifier: Modifier, + ) +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/ConnectedAppsScreenLayout.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/ConnectedAppsScreenLayout.kt new file mode 100644 index 0000000..5f8b52d --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/ConnectedAppsScreenLayout.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.contracts + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.appfunctions.agent.ui.screens.agentdemo.ConnectedAppsUiState + +/** + * Mandatory interface contract for Connected Apps screen layouts. + * Both Mobile and TV implementations MUST conform to this contract. + */ +interface ConnectedAppsScreenLayout { + @Composable + fun Content( + uiState: ConnectedAppsUiState, + onBack: () -> Unit, + onToggleApp: (String, Boolean) -> Unit, + modifier: Modifier, + ) +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/DebuggingScreenLayout.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/DebuggingScreenLayout.kt new file mode 100644 index 0000000..a21894b --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/DebuggingScreenLayout.kt @@ -0,0 +1,44 @@ +/* + * 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.ui.contracts + +import android.app.PendingIntent +import androidx.appfunctions.metadata.AppFunctionMetadata +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.appfunctions.agent.domain.appfunction.AppInfo +import com.example.appfunctions.agent.ui.screens.debugging.DebuggingUiState + +/** + * Mandatory interface contract for Debugging screen layouts. + * Both Mobile and TV implementations MUST conform to this contract. + */ +interface DebuggingScreenLayout { + @Composable + fun Content( + uiState: DebuggingUiState, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onFunctionInputsChange: (String, Map) -> Unit, + onInvoke: (AppFunctionMetadata) -> Unit, + onClearResult: () -> Unit, + onFunctionExpandedChange: (String, Boolean) -> Unit, + onLaunchPendingIntent: (PendingIntent) -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier, + ) +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/SettingsScreenLayout.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/SettingsScreenLayout.kt new file mode 100644 index 0000000..4798a75 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/contracts/SettingsScreenLayout.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.contracts + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * Mandatory interface contract for Settings screen layouts. + * Both Mobile and TV implementations MUST conform to this contract. + */ +interface SettingsScreenLayout { + @Composable + fun Content( + geminiApiKeyState: TextFieldState, + onOpenLicenses: () -> Unit, + onNavigateToConnectedApps: () -> Unit, + modifier: Modifier, + ) +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveInputContainer.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveInputContainer.kt new file mode 100644 index 0000000..d17ab71 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveInputContainer.kt @@ -0,0 +1,63 @@ +/* + * 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.ui.layout + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AdaptiveInputContainer( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val isTv = isTvFormFactor() + + if (isTv) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + modifier = modifier + .widthIn(max = 560.dp) + .fillMaxWidth(), + ) { + content() + } + } + } else { + ModalBottomSheet( + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + onDismissRequest = onDismissRequest, + dragHandle = null, + ) { + content() + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveMainNavigation.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveMainNavigation.kt new file mode 100644 index 0000000..245a9bd --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/AdaptiveMainNavigation.kt @@ -0,0 +1,128 @@ +/* + * 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.ui.layout + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState + +@Composable +fun AdaptiveMainNavigation( + navController: NavHostController, + items: List, + icons: List, + labels: List, + modifier: Modifier = Modifier, + content: @Composable (Modifier) -> Unit, +) { + val isTv = isTvFormFactor() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + if (isTv) { + Row( + modifier = modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets(0, 0, 0, 0)), + ) { + NavigationRail( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + modifier = Modifier.padding(horizontal = 4.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + items.forEachIndexed { index, screen -> + NavigationRailItem( + icon = { Icon(icons[index], contentDescription = labels[index]) }, + label = { Text(labels[index]) }, + selected = + currentDestination?.hierarchy?.any { + it.route?.startsWith(screen) == true + } == true, + onClick = { + navController.navigate(screen) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + ) + } + Spacer(modifier = Modifier.weight(1f)) + } + + content( + Modifier + .weight(1f) + .fillMaxSize(), + ) + } + } else { + Scaffold( + modifier = modifier.fillMaxSize(), + bottomBar = { + NavigationBar { + items.forEachIndexed { index, screen -> + NavigationBarItem( + icon = { Icon(icons[index], contentDescription = labels[index]) }, + label = { Text(labels[index]) }, + selected = + currentDestination?.hierarchy?.any { + it.route?.startsWith(screen) == true + } == true, + onClick = { + navController.navigate(screen) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + ) + } + } + }, + ) { innerPadding -> + content( + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/FormFactorUtils.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/FormFactorUtils.kt new file mode 100644 index 0000000..6f4cd21 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/layout/FormFactorUtils.kt @@ -0,0 +1,47 @@ +/* + * 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.ui.layout + +import android.content.pm.PackageManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +enum class FormFactor { + MOBILE, + TV, + WEAR, + AUTO, + XR, +} + +@Composable +fun rememberFormFactor(): FormFactor { + val context = LocalContext.current + return remember(context) { + val pm = context.packageManager + when { + pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) -> FormFactor.TV + pm.hasSystemFeature(PackageManager.FEATURE_WATCH) -> FormFactor.WEAR + pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) -> FormFactor.AUTO + pm.hasSystemFeature("android.hardware.xr.display") -> FormFactor.XR + else -> FormFactor.MOBILE + } + } +} + +@Composable +fun isTvFormFactor(): Boolean = rememberFormFactor() == FormFactor.TV diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileAgentDemoContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileAgentDemoContent.kt new file mode 100644 index 0000000..9bbf870 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileAgentDemoContent.kt @@ -0,0 +1,123 @@ +/* + * 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.ui.mobile.agentdemo + +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import com.example.appfunctions.agent.ui.contracts.AgentDemoScreenLayout +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentDemoLoadedScreen +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentDemoLoadingScreen +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiEvent +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiState +import com.example.appfunctions.agent.ui.screens.agentdemo.ChatHistorySidePanel +import kotlinx.coroutines.launch + +object MobileAgentDemoLayout : AgentDemoScreenLayout { + @Composable + override fun Content( + uiState: AgentUiState, + onEvent: (AgentUiEvent) -> Unit, + initialSidePanelVisible: Boolean, + modifier: Modifier, + ) { + MobileAgentDemoContent( + uiState = uiState, + onEvent = onEvent, + initialSidePanelVisible = initialSidePanelVisible, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MobileAgentDemoContent( + uiState: AgentUiState, + onEvent: (AgentUiEvent) -> Unit, + initialSidePanelVisible: Boolean = false, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val packageManager = context.packageManager + val focusManager = LocalFocusManager.current + + val containerSize = LocalConfiguration.current.screenWidthDp + val isWideScreen = containerSize >= 600 + + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { focusManager.clearFocus() } + + val content = + @Composable { + when (uiState) { + is AgentUiState.Loading -> { + AgentDemoLoadingScreen() + } + + is AgentUiState.Loaded -> { + AgentDemoLoadedScreen( + uiState = uiState, + onEvent = onEvent, + isWideScreen = isWideScreen, + drawerState = drawerState, + scope = scope, + packageManager = packageManager, + initialSidePanelVisible = initialSidePanelVisible, + ) + } + } + } + + if (isWideScreen) { + content() + } else { + val currentThread = (uiState as? AgentUiState.Loaded)?.currentThread + val threads = (uiState as? AgentUiState.Loaded)?.threads ?: emptyList() + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + drawerContainerColor = MaterialTheme.colorScheme.surface, + ) { + ChatHistorySidePanel( + threads = threads, + currentThread = currentThread, + onEvent = { event -> + onEvent(event) + scope.launch { drawerState.close() } + }, + ) + } + }, + ) { + content() + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileConnectedAppsContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileConnectedAppsContent.kt new file mode 100644 index 0000000..b53b571 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileConnectedAppsContent.kt @@ -0,0 +1,142 @@ +/* + * 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.ui.mobile.agentdemo + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.ui.contracts.ConnectedAppsScreenLayout +import com.example.appfunctions.agent.ui.screens.agentdemo.ConnectedAppsUiState + +object MobileConnectedAppsLayout : ConnectedAppsScreenLayout { + @Composable + override fun Content( + uiState: ConnectedAppsUiState, + onBack: () -> Unit, + onToggleApp: (String, Boolean) -> Unit, + modifier: Modifier, + ) { + MobileConnectedAppsContent( + uiState = uiState, + onBack = onBack, + onToggleApp = onToggleApp, + modifier = modifier, + ) + } +} + +@Composable +fun MobileConnectedAppsContent( + uiState: ConnectedAppsUiState, + onBack: () -> Unit, + onToggleApp: (String, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.clickable { onBack() }, + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.connected_apps_title), + style = MaterialTheme.typography.headlineMedium, + ) + } + }, + ) { paddingValues -> + Column( + modifier = + Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = 24.dp), + ) { + LazyColumn { + items( + items = uiState.connectedApps, + key = { app -> app.packageName }, + ) { app -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + ) { + if (app.icon != null) { + Image( + painter = rememberAsyncImagePainter(app.icon), + contentDescription = null, + modifier = Modifier.size(40.dp), + ) + } else { + Box(modifier = Modifier.size(40.dp).background(Color.Gray)) + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = app.label, + style = MaterialTheme.typography.bodyLarge, + ) + if (!app.description.isNullOrEmpty()) { + Text( + text = app.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Switch( + checked = app.isConnected, + onCheckedChange = { connected -> + onToggleApp(app.packageName, connected) + }, + ) + } + } + } + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileSettingsContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileSettingsContent.kt new file mode 100644 index 0000000..8df1232 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/agentdemo/MobileSettingsContent.kt @@ -0,0 +1,202 @@ +/* + * 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.ui.mobile.agentdemo + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.example.appfunctions.agent.BuildConfig +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.ui.contracts.SettingsScreenLayout + +object MobileSettingsLayout : SettingsScreenLayout { + @Composable + override fun Content( + geminiApiKeyState: TextFieldState, + onOpenLicenses: () -> Unit, + onNavigateToConnectedApps: () -> Unit, + modifier: Modifier, + ) { + MobileSettingsContent( + geminiApiKeyState = geminiApiKeyState, + onOpenLicenses = onOpenLicenses, + onNavigateToConnectedApps = onNavigateToConnectedApps, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MobileSettingsContent( + geminiApiKeyState: TextFieldState, + onOpenLicenses: () -> Unit, + onNavigateToConnectedApps: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.nav_settings), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.semantics { heading() }, + ) + } + }, + ) { paddingValues -> + Column( + modifier = + Modifier.fillMaxSize() + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .imePadding() + .verticalScroll(rememberScrollState()), + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_agent), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, + ) + } + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_gemini_api_key), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 8.dp), + ) + + if (BuildConfig.IS_RETAIL) { + OutlinedTextField( + value = geminiApiKeyState.text.toString(), + onValueChange = {}, + placeholder = { Text(stringResource(id = R.string.settings_gemini_api_key)) }, + visualTransformation = PasswordVisualTransformation(), + colors = OutlinedTextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + ), + readOnly = true, + modifier = Modifier.fillMaxWidth(), + ) + } else { + OutlinedTextField( + value = geminiApiKeyState.text.toString(), + onValueChange = { geminiApiKeyState.setTextAndPlaceCursorAtEnd(it) }, + placeholder = { Text(stringResource(id = R.string.settings_gemini_api_key)) }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.manage_connected_apps), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = { + Icon(imageVector = Icons.Default.Apps, contentDescription = null) + }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = + Modifier.clickable(role = Role.Button) { onNavigateToConnectedApps() }, + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.outlineVariant, + ) + + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_about), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, + ) + } + + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.settings_open_source_licenses), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = { + Icon(imageVector = Icons.Default.Info, contentDescription = null) + }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier.clickable(role = Role.Button) { onOpenLicenses() }, + ) + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/debugging/MobileDebuggingContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/debugging/MobileDebuggingContent.kt new file mode 100644 index 0000000..3099469 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/mobile/debugging/MobileDebuggingContent.kt @@ -0,0 +1,288 @@ +/* + * 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.ui.mobile.debugging + +import android.app.PendingIntent +import android.content.res.Resources +import androidx.appfunctions.metadata.AppFunctionMetadata +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.domain.appfunction.AppInfo +import com.example.appfunctions.agent.ui.contracts.DebuggingScreenLayout +import com.example.appfunctions.agent.ui.screens.debugging.AppDropdownItem +import com.example.appfunctions.agent.ui.screens.debugging.AppsGroupState +import com.example.appfunctions.agent.ui.screens.debugging.DebuggingUiState +import com.example.appfunctions.agent.ui.screens.debugging.FunctionsFoundContent +import com.example.appfunctions.agent.ui.screens.debugging.SearchAppResultState +import com.example.appfunctions.agent.ui.screens.debugging.TroubleshootResult + +object MobileDebuggingLayout : DebuggingScreenLayout { + @Composable + override fun Content( + uiState: DebuggingUiState, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onFunctionInputsChange: (String, Map) -> Unit, + onInvoke: (AppFunctionMetadata) -> Unit, + onClearResult: () -> Unit, + onFunctionExpandedChange: (String, Boolean) -> Unit, + onLaunchPendingIntent: (PendingIntent) -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier, + ) { + MobileDebuggingContent( + uiState = uiState, + onSearchQueryChanged = onSearchQueryChanged, + onAppSelected = onAppSelected, + onClearSelectedApp = onClearSelectedApp, + onFunctionInputsChange = onFunctionInputsChange, + onInvoke = onInvoke, + onClearResult = onClearResult, + onFunctionExpandedChange = onFunctionExpandedChange, + onLaunchPendingIntent = onLaunchPendingIntent, + onTogglePin = onTogglePin, + modifier = modifier, + ) + } +} + +@Composable +fun MobileDebuggingContent( + uiState: DebuggingUiState, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onFunctionInputsChange: (String, Map) -> Unit, + onInvoke: (AppFunctionMetadata) -> Unit, + onClearResult: () -> Unit, + onFunctionExpandedChange: (String, Boolean) -> Unit, + onLaunchPendingIntent: (PendingIntent) -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MobileAppDropdown( + modifier = Modifier.weight(1f).padding(horizontal = 8.dp), + appGroups = uiState.filteredApps, + selectedApp = uiState.selectedApp, + searchQuery = uiState.searchQuery, + onSearchQueryChanged = onSearchQueryChanged, + onAppSelected = onAppSelected, + onClearSelectedApp = onClearSelectedApp, + onTogglePin = onTogglePin, + ) + } + }, + ) { paddingValues -> + Box( + modifier = + Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()), + ) { + when (val searchAppResultState = uiState.searchAppResultState) { + is SearchAppResultState.Idle -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.debugging_select_app_prompt), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + } + } + is SearchAppResultState.FunctionsFoundState -> { + FunctionsFoundContent( + state = searchAppResultState, + onFunctionExpandedChange = onFunctionExpandedChange, + onFunctionInputsChange = onFunctionInputsChange, + onInvoke = onInvoke, + onClearResult = onClearResult, + onLaunchPendingIntent = onLaunchPendingIntent, + ) + } + is SearchAppResultState.TroubleshootUiState -> { + TroubleshootResult( + state = searchAppResultState, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MobileAppDropdown( + appGroups: AppsGroupState, + selectedApp: AppInfo?, + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + + val sections = appGroups.sections + val pinnedPackageNames = + remember(sections) { + sections + .find { it.titleRes == Resources.ID_NULL } + ?.apps + ?.map { it.packageName } + ?.toSet() ?: emptySet() + } + + OutlinedCard( + onClick = { expanded = !expanded }, + modifier = modifier.fillMaxWidth(), + ) { + ListItem( + headlineContent = { + Text( + text = selectedApp?.label ?: stringResource(R.string.debugging_select_app_prompt), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold, + ) + }, + supportingContent = if (selectedApp?.packageName != null) { + { Text(text = selectedApp.packageName) } + } else null, + leadingContent = if (selectedApp?.icon != null) { + { + Image( + bitmap = selectedApp.icon.toBitmap().asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } + } else null, + trailingContent = { + if (selectedApp != null) { + IconButton(onClick = { onClearSelectedApp() }) { + Icon(Icons.Filled.Clear, contentDescription = "Clear") + } + } else { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } + + if (expanded) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + ) { + Column { + OutlinedTextField( + value = searchQuery, + onValueChange = onSearchQueryChanged, + placeholder = { Text(stringResource(R.string.debugging_search_app)) }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + singleLine = true, + ) + + LazyColumn(modifier = Modifier.heightIn(max = 300.dp)) { + sections.forEach { section -> + item { + if (section.titleRes != Resources.ID_NULL) { + Text( + text = stringResource(section.titleRes), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + } + items(section.apps) { app -> + AppDropdownItem( + app = app, + isPinned = pinnedPackageNames.contains(app.packageName), + onAppSelected = { + onAppSelected(it) + expanded = false + focusManager.clearFocus() + }, + onSearchQueryChanged = onSearchQueryChanged, + onTogglePin = { onTogglePin(app) }, + onExpandedChange = { expanded = it }, + showPin = section.showPin, + ) + } + } + } + } + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/AgentDemoScreen.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/AgentDemoScreen.kt index 327c8cf..326c615 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/AgentDemoScreen.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/AgentDemoScreen.kt @@ -116,7 +116,7 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties import androidx.core.graphics.drawable.toBitmap -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.appfunctions.agent.R import com.example.appfunctions.agent.data.LlmModel @@ -126,7 +126,12 @@ import com.example.appfunctions.agent.data.db.entities.MessageRole import com.example.appfunctions.agent.data.db.entities.ThreadEntity import com.example.appfunctions.agent.domain.AgentStatus import com.example.appfunctions.agent.domain.appfunction.AppInfo +import com.example.appfunctions.agent.ui.contracts.AgentDemoScreenLayout +import com.example.appfunctions.agent.ui.layout.FormFactor +import com.example.appfunctions.agent.ui.layout.rememberFormFactor +import com.example.appfunctions.agent.ui.mobile.agentdemo.MobileAgentDemoLayout import com.example.appfunctions.agent.ui.screens.debugging.LazyExposedDropdownMenu +import com.example.appfunctions.agent.ui.tv.agentdemo.TvAgentDemoLayout import com.mikepenz.markdown.m3.Markdown import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -138,72 +143,24 @@ fun AgentDemoScreen(viewModel: AgentDemoViewModel = hiltViewModel()) { AgentDemoContent(uiState = uiState, onEvent = viewModel::onEvent) } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AgentDemoContent( uiState: AgentUiState, onEvent: (AgentUiEvent) -> Unit, initialSidePanelVisible: Boolean = false, ) { - val context = LocalContext.current - val packageManager = context.packageManager - val focusManager = LocalFocusManager.current - - val containerSize = LocalConfiguration.current.screenWidthDp - val isWideScreen = containerSize >= 600 - - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val scope = rememberCoroutineScope() - - LaunchedEffect(Unit) { focusManager.clearFocus() } - - val content = - @Composable { - when (uiState) { - is AgentUiState.Loading -> { - AgentDemoLoadingScreen() - } - - is AgentUiState.Loaded -> { - AgentDemoLoadedScreen( - uiState = uiState, - onEvent = onEvent, - isWideScreen = isWideScreen, - drawerState = drawerState, - scope = scope, - packageManager = packageManager, - initialSidePanelVisible = initialSidePanelVisible, - ) - } - } - } - - if (isWideScreen) { - content() - } else { - val currentThread = (uiState as? AgentUiState.Loaded)?.currentThread - val threads = (uiState as? AgentUiState.Loaded)?.threads ?: emptyList() - - ModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { - ModalDrawerSheet( - drawerContainerColor = MaterialTheme.colorScheme.surface, - ) { - ChatHistorySidePanel( - threads = threads, - currentThread = currentThread, - onEvent = { event -> - onEvent(event) - scope.launch { drawerState.close() } - }, - ) - } - }, - ) { - content() - } + val formFactor = rememberFormFactor() + val layout: AgentDemoScreenLayout = when (formFactor) { + FormFactor.TV -> TvAgentDemoLayout + FormFactor.WEAR, FormFactor.AUTO, FormFactor.XR, FormFactor.MOBILE -> MobileAgentDemoLayout } + + layout.Content( + uiState = uiState, + onEvent = onEvent, + initialSidePanelVisible = initialSidePanelVisible, + modifier = Modifier, + ) } @Composable @@ -224,6 +181,7 @@ fun AgentDemoLoadedScreen( packageManager: PackageManager, initialSidePanelVisible: Boolean = false, ) { + val focusManager = LocalFocusManager.current var messageText by remember { mutableStateOf(TextFieldValue("")) } var isSidePanelVisible by remember { mutableStateOf(initialSidePanelVisible) } var selectedAppPackageName by remember { mutableStateOf(null) } @@ -400,43 +358,17 @@ fun AgentDemoLoadedScreen( // Input area Box(modifier = Modifier.fillMaxWidth()) { - OutlinedTextField( - value = messageText, - onValueChange = { newValue -> - messageText = newValue - val currentText = newValue.text + com.example.appfunctions.agent.ui.components.TvSurfaceTextField( + value = messageText.text, + placeholder = stringResource(R.string.agent_demo_ask_agent), + onValueChange = { newString -> + messageText = TextFieldValue(newString) if (selectedAppPackageName != null && appMentionRegex != null) { - if (!appMentionRegex.containsMatchIn(currentText)) { + if (!appMentionRegex.containsMatchIn(newString)) { selectedAppPackageName = null } } }, - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 16.dp) - .onPreviewKeyEvent { keyEvent -> - if ( - (keyEvent.key == Key.Enter || keyEvent.key == Key.NumPadEnter) && - keyEvent.type == KeyEventType.KeyDown - ) { - sendMessage() - true - } else { - false - } - }, - enabled = uiState.status == AgentStatus.Idle, - shape = CircleShape, - placeholder = { Text(stringResource(R.string.agent_demo_ask_agent)) }, - visualTransformation = visualTransformation, - colors = - OutlinedTextFieldDefaults.colors( - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceBright, - focusedContainerColor = MaterialTheme.colorScheme.surfaceBright, - unfocusedBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - ), trailingIcon = { IconButton( onClick = sendMessage, @@ -451,6 +383,7 @@ fun AgentDemoLoadedScreen( ) } }, + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), ) if (showAutocomplete && filteredApps.isNotEmpty()) { diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/ConnectedAppsScreen.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/ConnectedAppsScreen.kt index 8de3114..256f255 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/ConnectedAppsScreen.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/ConnectedAppsScreen.kt @@ -44,11 +44,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.rememberAsyncImagePainter import com.example.appfunctions.agent.R import com.example.appfunctions.agent.domain.appfunction.ConnectedAppInfo +import com.example.appfunctions.agent.ui.contracts.ConnectedAppsScreenLayout +import com.example.appfunctions.agent.ui.layout.FormFactor +import com.example.appfunctions.agent.ui.layout.rememberFormFactor +import com.example.appfunctions.agent.ui.mobile.agentdemo.MobileConnectedAppsLayout +import com.example.appfunctions.agent.ui.tv.agentdemo.TvConnectedAppsLayout /** Stateful composable for the Connected Apps screen. */ @Composable @@ -73,74 +78,18 @@ fun ConnectedAppsScreenContent( onBack: () -> Unit, onToggleApp: (String, Boolean) -> Unit, ) { - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Unspecified, - topBar = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - modifier = Modifier.clickable { onBack() }, - ) - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = stringResource(id = R.string.connected_apps_title), - style = MaterialTheme.typography.headlineMedium, - ) - } - }, - ) { paddingValues -> - Column( - modifier = - Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = 24.dp), - ) { - LazyColumn { - items( - items = uiState.connectedApps, - key = { app -> app.packageName }, - ) { app -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), - ) { - if (app.icon != null) { - Image( - painter = rememberAsyncImagePainter(app.icon), - contentDescription = null, - modifier = Modifier.size(40.dp), - ) - } else { - Box(modifier = Modifier.size(40.dp).background(Color.Gray)) - } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = app.label, - style = MaterialTheme.typography.bodyLarge, - ) - if (!app.description.isNullOrEmpty()) { - Text( - text = app.description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - Switch( - checked = app.isConnected, - onCheckedChange = { connected -> - onToggleApp(app.packageName, connected) - }, - ) - } - } - } - } + val formFactor = rememberFormFactor() + val layout: ConnectedAppsScreenLayout = when (formFactor) { + FormFactor.TV -> TvConnectedAppsLayout + FormFactor.WEAR, FormFactor.AUTO, FormFactor.XR, FormFactor.MOBILE -> MobileConnectedAppsLayout } + + layout.Content( + uiState = uiState, + onBack = onBack, + onToggleApp = onToggleApp, + modifier = Modifier, + ) } @Preview(showBackground = true) diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/SettingsScreen.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/SettingsScreen.kt index b455b65..51d90b8 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/SettingsScreen.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/agentdemo/SettingsScreen.kt @@ -59,13 +59,19 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.example.appfunctions.agent.BuildConfig import com.example.appfunctions.agent.R import com.google.android.gms.oss.licenses.v2.OssLicensesMenuActivity import kotlin.OptIn /** Stateful composable for the Settings screen. */ +import com.example.appfunctions.agent.ui.contracts.SettingsScreenLayout +import com.example.appfunctions.agent.ui.layout.FormFactor +import com.example.appfunctions.agent.ui.layout.rememberFormFactor +import com.example.appfunctions.agent.ui.mobile.agentdemo.MobileSettingsLayout +import com.example.appfunctions.agent.ui.tv.agentdemo.TvSettingsLayout + @Composable fun SettingsScreen( onNavigateToConnectedApps: () -> Unit, @@ -87,148 +93,24 @@ fun SettingsScreen( } /** Stateless composable for the Settings screen, allowing for previews and easier testing. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreenContent( geminiApiKeyState: TextFieldState, onOpenLicenses: () -> Unit, onNavigateToConnectedApps: () -> Unit, ) { - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Unspecified, - topBar = { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.nav_settings), - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.semantics { heading() }, - ) - } - }, - ) { paddingValues -> - Column( - modifier = - Modifier.fillMaxSize() - .padding(paddingValues) - .consumeWindowInsets(paddingValues) - .imePadding() - .verticalScroll(rememberScrollState()), - ) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Text( - text = stringResource(id = R.string.settings_agent), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, - ) - } - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Text( - text = stringResource(id = R.string.settings_gemini_api_key), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 8.dp), - ) - - // NOTE: We use the value/onValueChange overload here because the new state - // overload might not support simple PasswordVisualTransformation as - // easily in this version. - if (BuildConfig.IS_RETAIL) { - OutlinedTextField( - value = geminiApiKeyState.text.toString(), - onValueChange = {}, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = CircleShape, - colors = - OutlinedTextFieldDefaults.colors( - unfocusedContainerColor = - MaterialTheme.colorScheme.surfaceBright, - focusedContainerColor = - MaterialTheme.colorScheme.surfaceBright, - ), - ) - } else { - OutlinedTextField( - value = geminiApiKeyState.text.toString(), - onValueChange = { geminiApiKeyState.setTextAndPlaceCursorAtEnd(it) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - shape = CircleShape, - colors = - OutlinedTextFieldDefaults.colors( - unfocusedContainerColor = - MaterialTheme.colorScheme.surfaceBright, - focusedContainerColor = - MaterialTheme.colorScheme.surfaceBright, - ), - ) - } - } - - ListItem( - headlineContent = { - Text( - text = stringResource(id = R.string.manage_connected_apps), - style = MaterialTheme.typography.bodyLarge, - ) - }, - leadingContent = { - Icon(imageVector = Icons.Default.Apps, contentDescription = null) - }, - trailingContent = { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - ) - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - modifier = - Modifier.clickable(role = Role.Button) { onNavigateToConnectedApps() }, - ) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - color = MaterialTheme.colorScheme.outlineVariant, - ) - - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Text( - text = stringResource(id = R.string.settings_about), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, - ) - } - - ListItem( - headlineContent = { - Text( - text = stringResource(id = R.string.settings_open_source_licenses), - style = MaterialTheme.typography.bodyLarge, - ) - }, - leadingContent = { - Icon(imageVector = Icons.Default.Info, contentDescription = null) - }, - trailingContent = { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - ) - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - modifier = Modifier.clickable(role = Role.Button) { onOpenLicenses() }, - ) - } + val formFactor = rememberFormFactor() + val layout: SettingsScreenLayout = when (formFactor) { + FormFactor.TV -> TvSettingsLayout + FormFactor.WEAR, FormFactor.AUTO, FormFactor.XR, FormFactor.MOBILE -> MobileSettingsLayout } + + layout.Content( + geminiApiKeyState = geminiApiKeyState, + onOpenLicenses = onOpenLicenses, + onNavigateToConnectedApps = onNavigateToConnectedApps, + modifier = Modifier, + ) } @Preview(showBackground = true) diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionDataTypeInput.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionDataTypeInput.kt index 8eff41a..4cd7255 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionDataTypeInput.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/AppFunctionDataTypeInput.kt @@ -37,7 +37,14 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -49,12 +56,15 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import com.example.appfunctions.agent.ui.layout.AdaptiveInputContainer import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults +import androidx.compose.foundation.layout.widthIn +import androidx.compose.ui.window.Dialog import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults @@ -180,8 +190,7 @@ fun AppFunctionDataTypeInput( ) if (showSheet) { - ModalBottomSheet( - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + AdaptiveInputContainer( onDismissRequest = { showSheet = false }, ) { AppFunctionObjectTypeSheetContent( @@ -227,8 +236,7 @@ fun AppFunctionDataTypeInput( ) if (showSheet) { - ModalBottomSheet( - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + AdaptiveInputContainer( onDismissRequest = { showSheet = false }, ) { AppFunctionObjectTypeSheetContent( @@ -275,30 +283,14 @@ fun PrimitiveTextInput( modifier: Modifier = Modifier, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, ) { - OutlinedTextField( + com.example.appfunctions.agent.ui.components.TvSurfaceTextField( value = value, onValueChange = onValueChange, - label = { - Row { - Text(label) - if (isRequired) { - Text( - text = stringResource(R.string.debugging_required_indicator), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.labelSmall, - ) - } - } - }, - modifier = modifier.height(72.dp).fillMaxWidth(), - singleLine = true, + placeholder = label, + label = if (isRequired) "$label *" else label, + modifier = modifier.fillMaxWidth(), keyboardOptions = keyboardOptions, shape = MaterialTheme.shapes.medium, - colors = - OutlinedTextFieldDefaults.colors( - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, - focusedBorderColor = MaterialTheme.colorScheme.outlineVariant, - ), ) } 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 dadf8f8..a332116 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 @@ -31,8 +31,10 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -57,7 +59,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -70,6 +74,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.ui.layout.isTvFormFactor import com.example.appfunctions.agent.ui.theme.GoogleSansCodeFontFamily @Composable @@ -94,12 +99,22 @@ fun AppFunctionItem( }, ) + val isTv = isTvFormFactor() + var isCardFocused by remember { mutableStateOf(false) } + Surface( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .onFocusChanged { isCardFocused = it.hasFocus }, tonalElevation = tonalElevation, shadowElevation = shadowElevation, shape = MaterialTheme.shapes.large, color = surfaceColor, + border = if (isTv && isCardFocused) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else { + BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + }, ) { Column( modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 8.dp, bottom = 16.dp), diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingScreen.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingScreen.kt index 1e58c4c..2158ec0 100644 --- a/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingScreen.kt +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/screens/debugging/DebuggingScreen.kt @@ -18,20 +18,25 @@ package com.example.appfunctions.agent.ui.screens.debugging import android.app.PendingIntent import android.content.res.Resources import androidx.appfunctions.metadata.AppFunctionMetadata +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.outlined.PushPin @@ -43,7 +48,10 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -57,19 +65,32 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.appfunctions.agent.R import com.example.appfunctions.agent.domain.appfunction.AppInfo +import com.example.appfunctions.agent.ui.contracts.DebuggingScreenLayout +import com.example.appfunctions.agent.ui.layout.rememberFormFactor +import com.example.appfunctions.agent.ui.layout.FormFactor +import com.example.appfunctions.agent.ui.mobile.debugging.MobileDebuggingLayout import com.example.appfunctions.agent.ui.theme.AppFunctionsAgentTheme +import com.example.appfunctions.agent.ui.tv.debugging.TvDebuggingLayout @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -108,188 +129,29 @@ fun DebuggingScreenContent( onLaunchPendingIntent: (PendingIntent) -> Unit, onTogglePin: (AppInfo) -> Unit, ) { - val focusManager = LocalFocusManager.current - LaunchedEffect(Unit) { focusManager.clearFocus() } - - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = Color.Unspecified, - topBar = { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - // Searchable Dropdown - AppDropdown( - modifier = Modifier.weight(1f).padding(horizontal = 8.dp), - appGroups = uiState.filteredApps, - searchQuery = uiState.searchQuery, - onSearchQueryChanged = onSearchQueryChanged, - onAppSelected = onAppSelected, - onClearSelectedApp = onClearSelectedApp, - onTogglePin = onTogglePin, - ) - } - }, - ) { paddingValues -> - Box( - modifier = - Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()), - ) { - when (val searchAppResultState = uiState.searchAppResultState) { - is SearchAppResultState.Idle -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(R.string.debugging_select_app_prompt), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - ) - } - } - is SearchAppResultState.FunctionsFoundState -> { - FunctionsFoundContent( - state = searchAppResultState, - onFunctionExpandedChange = onFunctionExpandedChange, - onFunctionInputsChange = onFunctionInputsChange, - onInvoke = onInvoke, - onClearResult = onClearResult, - onLaunchPendingIntent = onLaunchPendingIntent, - ) - } - is SearchAppResultState.TroubleshootUiState -> { - TroubleshootResult( - state = searchAppResultState, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } + val formFactor = rememberFormFactor() + val layout: DebuggingScreenLayout = when (formFactor) { + FormFactor.TV -> TvDebuggingLayout + FormFactor.WEAR, FormFactor.AUTO, FormFactor.XR, FormFactor.MOBILE -> MobileDebuggingLayout } -} -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AppDropdown( - appGroups: AppsGroupState, - searchQuery: String, - onSearchQueryChanged: (String) -> Unit, - onAppSelected: (AppInfo) -> Unit, - onClearSelectedApp: () -> Unit, - onTogglePin: (AppInfo) -> Unit, - modifier: Modifier = Modifier, -) { - var expanded by remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - modifier = modifier, - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - ) { - Surface( - modifier = Modifier.padding(bottom = 8.dp), - shadowElevation = 2.dp, - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceBright, - ) { - OutlinedTextField( - value = searchQuery, - shape = CircleShape, - singleLine = true, - placeholder = { Text(text = stringResource(R.string.debugging_search_app)) }, - onValueChange = { - onSearchQueryChanged(it) - expanded = true - }, - trailingIcon = { - Row( - modifier = Modifier.padding(end = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (searchQuery.isNotEmpty()) { - IconButton(onClick = { onClearSelectedApp() }) { - Icon(Icons.Filled.Clear, contentDescription = "Clear") - } - } - } - }, - colors = - ExposedDropdownMenuDefaults.outlinedTextFieldColors( - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceBright, - focusedContainerColor = MaterialTheme.colorScheme.surfaceBright, - unfocusedBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - ), - modifier = - Modifier.fillMaxWidth() - .menuAnchor( - ExposedDropdownMenuAnchorType.Companion.PrimaryEditable, - enabled = true, - ), - ) - } - - val sections = appGroups.sections - val pinnedPackageNames = - remember(sections) { - sections - .find { it.titleRes == Resources.ID_NULL } - ?.apps - ?.map { it.packageName } - ?.toSet() ?: emptySet() - } - - LazyExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier.exposedDropdownSize(), - containerColor = MaterialTheme.colorScheme.surfaceBright, - shape = RoundedCornerShape(28.dp), - ) { - sections.forEachIndexed { index, section -> - if (index > 0) { - item { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp), - ) - } - } - - // Not showing pinned section title - if (index != 0) { - item { - Text( - text = stringResource(id = section.titleRes), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - ) - } - } - - items( - items = section.apps, - key = { app -> "${section.titleRes}_${app.packageName}" }, - ) { app -> - AppDropdownItem( - app = app, - isPinned = pinnedPackageNames.contains(app.packageName), - onAppSelected = onAppSelected, - onSearchQueryChanged = onSearchQueryChanged, - onTogglePin = onTogglePin, - onExpandedChange = { expanded = it }, - showPin = section.showPin, - ) - } - } - } - } + layout.Content( + uiState = uiState, + onSearchQueryChanged = onSearchQueryChanged, + onAppSelected = onAppSelected, + onClearSelectedApp = onClearSelectedApp, + onFunctionInputsChange = onFunctionInputsChange, + onInvoke = onInvoke, + onClearResult = onClearResult, + onFunctionExpandedChange = onFunctionExpandedChange, + onLaunchPendingIntent = onLaunchPendingIntent, + onTogglePin = onTogglePin, + modifier = Modifier, + ) } @Composable -private fun AppDropdownItem( +internal fun AppDropdownItem( app: AppInfo, isPinned: Boolean, onAppSelected: (AppInfo) -> Unit, diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvAgentDemoContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvAgentDemoContent.kt new file mode 100644 index 0000000..6fc5609 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvAgentDemoContent.kt @@ -0,0 +1,84 @@ +/* + * 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.ui.tv.agentdemo + +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import com.example.appfunctions.agent.ui.contracts.AgentDemoScreenLayout +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentDemoLoadedScreen +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentDemoLoadingScreen +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiEvent +import com.example.appfunctions.agent.ui.screens.agentdemo.AgentUiState + +object TvAgentDemoLayout : AgentDemoScreenLayout { + @Composable + override fun Content( + uiState: AgentUiState, + onEvent: (AgentUiEvent) -> Unit, + initialSidePanelVisible: Boolean, + modifier: Modifier, + ) { + TvAgentDemoContent( + uiState = uiState, + onEvent = onEvent, + initialSidePanelVisible = initialSidePanelVisible, + modifier = modifier, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TvAgentDemoContent( + uiState: AgentUiState, + onEvent: (AgentUiEvent) -> Unit, + initialSidePanelVisible: Boolean = false, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val packageManager = context.packageManager + val focusManager = LocalFocusManager.current + + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { focusManager.clearFocus() } + + when (uiState) { + is AgentUiState.Loading -> { + AgentDemoLoadingScreen() + } + + is AgentUiState.Loaded -> { + AgentDemoLoadedScreen( + uiState = uiState, + onEvent = onEvent, + isWideScreen = true, // Force landscape dual-pane for TV D-Pad navigation + drawerState = drawerState, + scope = scope, + packageManager = packageManager, + initialSidePanelVisible = initialSidePanelVisible, + ) + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvConnectedAppsContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvConnectedAppsContent.kt new file mode 100644 index 0000000..588ffcb --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvConnectedAppsContent.kt @@ -0,0 +1,164 @@ +/* + * 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.ui.tv.agentdemo + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.ui.contracts.ConnectedAppsScreenLayout +import com.example.appfunctions.agent.ui.screens.agentdemo.ConnectedAppsUiState + +object TvConnectedAppsLayout : ConnectedAppsScreenLayout { + @Composable + override fun Content( + uiState: ConnectedAppsUiState, + onBack: () -> Unit, + onToggleApp: (String, Boolean) -> Unit, + modifier: Modifier, + ) { + TvConnectedAppsContent( + uiState = uiState, + onBack = onBack, + onToggleApp = onToggleApp, + modifier = modifier, + ) + } +} + +@Composable +fun TvConnectedAppsContent( + uiState: ConnectedAppsUiState, + onBack: () -> Unit, + onToggleApp: (String, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + ) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.connected_apps_title), + style = MaterialTheme.typography.headlineMedium, + ) + } + }, + ) { paddingValues -> + Column( + modifier = + Modifier.fillMaxSize().padding(paddingValues).padding(horizontal = 24.dp), + ) { + LazyColumn { + items( + items = uiState.connectedApps, + key = { app -> app.packageName }, + ) { app -> + var isRowFocused by remember { mutableStateOf(false) } + Surface( + onClick = { onToggleApp(app.packageName, !app.isConnected) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .onFocusChanged { isRowFocused = it.isFocused }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = if (isRowFocused) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else null, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + if (app.icon != null) { + Image( + painter = rememberAsyncImagePainter(app.icon), + contentDescription = null, + modifier = Modifier.size(40.dp), + ) + } else { + Box(modifier = Modifier.size(40.dp).background(Color.Gray)) + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = app.label, + style = MaterialTheme.typography.bodyLarge, + ) + if (!app.description.isNullOrEmpty()) { + Text( + text = app.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Switch( + checked = app.isConnected, + onCheckedChange = null, + ) + } + } + } + } + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvSettingsContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvSettingsContent.kt new file mode 100644 index 0000000..f1a3e75 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/agentdemo/TvSettingsContent.kt @@ -0,0 +1,222 @@ +/* + * 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.ui.tv.agentdemo + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.example.appfunctions.agent.BuildConfig +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.ui.components.TvSurfaceTextField +import com.example.appfunctions.agent.ui.contracts.SettingsScreenLayout + +object TvSettingsLayout : SettingsScreenLayout { + @Composable + override fun Content( + geminiApiKeyState: TextFieldState, + onOpenLicenses: () -> Unit, + onNavigateToConnectedApps: () -> Unit, + modifier: Modifier, + ) { + TvSettingsContent( + geminiApiKeyState = geminiApiKeyState, + onOpenLicenses = onOpenLicenses, + onNavigateToConnectedApps = onNavigateToConnectedApps, + modifier = modifier, + ) + } +} + +@Composable +fun TvSettingsContent( + geminiApiKeyState: TextFieldState, + onOpenLicenses: () -> Unit, + onNavigateToConnectedApps: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.nav_settings), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.semantics { heading() }, + ) + } + }, + ) { paddingValues -> + Column( + modifier = + Modifier.fillMaxSize() + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .imePadding() + .verticalScroll(rememberScrollState()), + ) { + Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_agent), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, + ) + } + Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_gemini_api_key), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 8.dp), + ) + + if (BuildConfig.IS_RETAIL) { + TvSurfaceTextField( + value = geminiApiKeyState.text.toString(), + onValueChange = {}, + placeholder = stringResource(id = R.string.settings_gemini_api_key), + modifier = Modifier.fillMaxWidth(), + ) + } else { + TvSurfaceTextField( + value = geminiApiKeyState.text.toString(), + onValueChange = { geminiApiKeyState.setTextAndPlaceCursorAtEnd(it) }, + placeholder = stringResource(id = R.string.settings_gemini_api_key), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + var isAppsFocused by remember { mutableStateOf(false) } + Surface( + onClick = onNavigateToConnectedApps, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .onFocusChanged { isAppsFocused = it.isFocused }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = if (isAppsFocused) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else null, + ) { + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.manage_connected_apps), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = { + Icon(imageVector = Icons.Default.Apps, contentDescription = null) + }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp), + color = MaterialTheme.colorScheme.outlineVariant, + ) + + Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)) { + Text( + text = stringResource(id = R.string.settings_about), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 8.dp).semantics { heading() }, + ) + } + + var isLicensesFocused by remember { mutableStateOf(false) } + Surface( + onClick = onOpenLicenses, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .onFocusChanged { isLicensesFocused = it.isFocused }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceContainerLow, + border = if (isLicensesFocused) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else null, + ) { + ListItem( + headlineContent = { + Text( + text = stringResource(id = R.string.settings_open_source_licenses), + style = MaterialTheme.typography.bodyLarge, + ) + }, + leadingContent = { + Icon(imageVector = Icons.Default.Info, contentDescription = null) + }, + trailingContent = { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } + } + } +} diff --git a/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/debugging/TvDebuggingContent.kt b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/debugging/TvDebuggingContent.kt new file mode 100644 index 0000000..a61a0b2 --- /dev/null +++ b/agent/app/src/main/java/com/example/appfunctions/agent/ui/tv/debugging/TvDebuggingContent.kt @@ -0,0 +1,327 @@ +/* + * 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.ui.tv.debugging + +import android.app.PendingIntent +import android.content.res.Resources +import androidx.appfunctions.metadata.AppFunctionMetadata +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.example.appfunctions.agent.R +import com.example.appfunctions.agent.domain.appfunction.AppInfo +import com.example.appfunctions.agent.ui.components.TvSurfaceTextField +import com.example.appfunctions.agent.ui.contracts.DebuggingScreenLayout +import com.example.appfunctions.agent.ui.screens.debugging.AppDropdownItem +import com.example.appfunctions.agent.ui.screens.debugging.AppsGroupState +import com.example.appfunctions.agent.ui.screens.debugging.DebuggingUiState +import com.example.appfunctions.agent.ui.screens.debugging.FunctionsFoundContent +import com.example.appfunctions.agent.ui.screens.debugging.SearchAppResultState +import com.example.appfunctions.agent.ui.screens.debugging.TroubleshootResult +import kotlinx.coroutines.android.awaitFrame + +object TvDebuggingLayout : DebuggingScreenLayout { + @Composable + override fun Content( + uiState: DebuggingUiState, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onFunctionInputsChange: (String, Map) -> Unit, + onInvoke: (AppFunctionMetadata) -> Unit, + onClearResult: () -> Unit, + onFunctionExpandedChange: (String, Boolean) -> Unit, + onLaunchPendingIntent: (PendingIntent) -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier, + ) { + TvDebuggingContent( + uiState = uiState, + onSearchQueryChanged = onSearchQueryChanged, + onAppSelected = onAppSelected, + onClearSelectedApp = onClearSelectedApp, + onFunctionInputsChange = onFunctionInputsChange, + onInvoke = onInvoke, + onClearResult = onClearResult, + onFunctionExpandedChange = onFunctionExpandedChange, + onLaunchPendingIntent = onLaunchPendingIntent, + onTogglePin = onTogglePin, + modifier = modifier, + ) + } +} + +@Composable +fun TvDebuggingContent( + uiState: DebuggingUiState, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onFunctionInputsChange: (String, Map) -> Unit, + onInvoke: (AppFunctionMetadata) -> Unit, + onClearResult: () -> Unit, + onFunctionExpandedChange: (String, Boolean) -> Unit, + onLaunchPendingIntent: (PendingIntent) -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + containerColor = Color.Unspecified, + topBar = { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TvAppDropdown( + modifier = Modifier.weight(1f), + appGroups = uiState.filteredApps, + searchQuery = uiState.searchQuery, + onSearchQueryChanged = onSearchQueryChanged, + onAppSelected = onAppSelected, + onClearSelectedApp = onClearSelectedApp, + onTogglePin = onTogglePin, + ) + } + }, + ) { paddingValues -> + Box( + modifier = + Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()), + ) { + when (val searchAppResultState = uiState.searchAppResultState) { + is SearchAppResultState.Idle -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.debugging_select_app_prompt), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + } + } + is SearchAppResultState.FunctionsFoundState -> { + FunctionsFoundContent( + state = searchAppResultState, + onFunctionExpandedChange = onFunctionExpandedChange, + onFunctionInputsChange = onFunctionInputsChange, + onInvoke = onInvoke, + onClearResult = onClearResult, + onLaunchPendingIntent = onLaunchPendingIntent, + ) + } + is SearchAppResultState.TroubleshootUiState -> { + TroubleshootResult( + state = searchAppResultState, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} + +@Composable +private fun TvAppDropdown( + appGroups: AppsGroupState, + searchQuery: String, + onSearchQueryChanged: (String) -> Unit, + onAppSelected: (AppInfo) -> Unit, + onClearSelectedApp: () -> Unit, + onTogglePin: (AppInfo) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + + val sections = appGroups.sections + val pinnedPackageNames = + remember(sections) { + sections + .find { it.titleRes == Resources.ID_NULL } + ?.apps + ?.map { it.packageName } + ?.toSet() ?: emptySet() + } + + val dropdownFocusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + awaitFrame() + dropdownFocusRequester.requestFocus() + } + + Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + TvSurfaceTextField( + value = searchQuery, + placeholder = stringResource(R.string.debugging_search_app), + onValueChange = onSearchQueryChanged, + trailingIcon = if (searchQuery.isNotEmpty()) { + { + IconButton(onClick = { onClearSelectedApp() }) { + Icon(Icons.Filled.Clear, contentDescription = "Clear") + } + } + } else null, + modifier = Modifier.weight(1f), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Surface( + onClick = { expanded = !expanded }, + modifier = Modifier + .focusRequester(dropdownFocusRequester) + .onPreviewKeyEvent { keyEvent -> + if (expanded && keyEvent.key == Key.Back && keyEvent.type == KeyEventType.KeyUp) { + expanded = false + true + } else false + }, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceBright, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Apps", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "Toggle Dropdown", + ) + } + } + } + + if (expanded) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.key == Key.Back && keyEvent.type == KeyEventType.KeyUp) { + expanded = false + true + } else false + }, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + ), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + ) { + LazyColumn(modifier = Modifier.padding(8.dp)) { + sections.forEachIndexed { index, section -> + if (index > 0) { + item { + HorizontalDivider( + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), + ) + } + } + + if (index != 0) { + item { + Text( + text = stringResource(id = section.titleRes), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), + ) + } + } + + items( + items = section.apps, + key = { app -> "${section.titleRes}_${app.packageName}" }, + ) { app -> + AppDropdownItem( + app = app, + isPinned = pinnedPackageNames.contains(app.packageName), + onAppSelected = { + onAppSelected(it) + expanded = false + focusManager.clearFocus() + }, + onSearchQueryChanged = onSearchQueryChanged, + onTogglePin = onTogglePin, + onExpandedChange = { expanded = it }, + showPin = section.showPin, + ) + } + } + } + } + } + } +} diff --git a/agent/app/src/main/res/drawable-xhdpi/tv_banner.png b/agent/app/src/main/res/drawable-xhdpi/tv_banner.png new file mode 100644 index 0000000..c074574 Binary files /dev/null and b/agent/app/src/main/res/drawable-xhdpi/tv_banner.png differ diff --git a/agent/app/src/main/res/drawable/ic_launcher_background.xml b/agent/app/src/main/res/drawable/ic_launcher_background.xml index 6a1535a..909c853 100644 --- a/agent/app/src/main/res/drawable/ic_launcher_background.xml +++ b/agent/app/src/main/res/drawable/ic_launcher_background.xml @@ -21,166 +21,11 @@ android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:strokeColor="#3300E5FF" /> diff --git a/agent/app/src/main/res/drawable/ic_launcher_foreground.xml b/agent/app/src/main/res/drawable/ic_launcher_foreground.xml index 2aaf647..46770fb 100644 --- a/agent/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/agent/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,3 +1,4 @@ + - - - - - - - - + + android:fillColor="#00E5FF" + android:pathData="M54,24 A30,30 0 1,0 54,84 A30,30 0 1,0 54,24 Z" + android:fillAlpha="0.15" /> + + + + + + \ No newline at end of file diff --git a/agent/app/src/main/res/drawable/tv_banner.png b/agent/app/src/main/res/drawable/tv_banner.png new file mode 100644 index 0000000..c074574 Binary files /dev/null and b/agent/app/src/main/res/drawable/tv_banner.png differ diff --git a/agent/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/agent/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/agent/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/agent/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/agent/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/agent/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/agent/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/agent/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/agent/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/agent/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/agent/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/agent/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/agent/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/agent/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/agent/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ