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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agent/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-feature android:name="android.software.leanback" android:required="false" />

<application
android:name=".AppFunctionsApplication"
android:allowBackup="true"
android:banner="@mipmap/ic_launcher"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
Expand All @@ -38,11 +41,13 @@

<activity
android:name=".MainActivity"
android:banner="@mipmap/ic_launcher"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
},
) {
Comment thread
pflammertsma marked this conversation as resolved.
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()
}
}
}
}
Loading