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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.google.ai.sample.util.AppNamePackageMapper
import com.google.ai.sample.util.AppOpenFeedbackPreferences
import com.google.ai.sample.util.Command
import com.google.ai.sample.util.CoordinateParser
import com.google.ai.sample.util.TermuxExecutionModePreferences
import com.google.ai.sample.util.TermuxFeedbackPreferences
import com.google.ai.sample.util.TermuxOutputPreferences
import java.io.File
Expand Down Expand Up @@ -595,13 +596,14 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
Log.e(TAG, "Failed to register Termux result receiver", t)
}

val executeInBackground = TermuxExecutionModePreferences.executeInBackground(applicationContext)
val intent = Intent("com.termux.RUN_COMMAND").apply {
`package` = termuxPackage
setClassName(termuxPackage, runCommandServiceClass)
putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash")
putExtra("com.termux.RUN_COMMAND_ARGUMENTS", arrayOf("-lc", trimmedCommand))
putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home")
putExtra("com.termux.RUN_COMMAND_BACKGROUND", false)
putExtra("com.termux.RUN_COMMAND_BACKGROUND", executeInBackground)
putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0)
putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell")
putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import com.google.ai.sample.ScreenOperatorAccessibilityService
import com.google.ai.sample.util.Command
import com.google.ai.sample.util.SystemMessageEntry
import com.google.ai.sample.util.SystemMessageEntryPreferences
import com.google.ai.sample.util.TermuxExecutionModePreferences
import com.google.ai.sample.util.TermuxFeedbackPreferences
import com.google.ai.sample.util.TermuxOutputPreferences
import com.google.ai.sample.util.UriSaver
Expand Down Expand Up @@ -159,6 +160,9 @@ fun PhotoReasoningScreen(
val focusManager = LocalFocusManager.current
val messages by chatMessages.collectAsState()
var isTermuxPermissionRequestPending by rememberSaveable { mutableStateOf(false) }
var executeTermuxInBackground by rememberSaveable {
mutableStateOf(TermuxExecutionModePreferences.executeInBackground(context))
}

LaunchedEffect(Unit) {
systemMessageEntries = SystemMessageEntryPreferences.loadEntries(context)
Expand Down Expand Up @@ -440,50 +444,81 @@ fun PhotoReasoningScreen(
onValueChange = onUserQuestionChanged,
modifier = Modifier.weight(1f).padding(end = 8.dp)
)
IconButton(
onClick = {
val mainActivity = context as? MainActivity

// Always check accessibility service (needed for both live and regular models)
if (!isAccessibilityServiceEnabled) {
onEnableAccessibilityService()
Toast.makeText(context, "Enable the Accessibility service for Screen Operator", Toast.LENGTH_LONG).show()
return@IconButton
Column(modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)) {
IconButton(
onClick = {
val updatedExecuteInBackground = !executeTermuxInBackground
executeTermuxInBackground = updatedExecuteInBackground
TermuxExecutionModePreferences.setExecuteInBackground(
context,
updatedExecuteInBackground
)
val toastMessage = if (updatedExecuteInBackground) {
"Termux commands are executed in the background"
} else {
"Termux commands are executed in the foreground"
}
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show()
},
modifier = Modifier.padding(bottom = 4.dp).drawBehind {
drawCircle(
color = Color.Black,
radius = size.minDimension / 2,
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx())
)
}
) {
Text(
if (executeTermuxInBackground) "TB" else "TF",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
}
IconButton(
onClick = {
val mainActivity = context as? MainActivity

if (userQuestion.isBlank()) {
return@IconButton
}
// Always check accessibility service (needed for both live and regular models)
if (!isAccessibilityServiceEnabled) {
onEnableAccessibilityService()
Toast.makeText(context, "Enable the Accessibility service for Screen Operator", Toast.LENGTH_LONG).show()
return@IconButton
}

if (mainActivity == null) {
handleTermuxRunCommandPermissionDenied()
return@IconButton
}
if (userQuestion.isBlank()) {
return@IconButton
}

// Human Expert uses its own MediaProjection for WebRTC, not ScreenCaptureService.
// Termux is still required for every model, including Human Expert, via the shared send path below.
val currentModel = com.google.ai.sample.GenerativeAiViewModelFactory.getCurrentModel()
val requiresScreenCapturePermission = currentModel.supportsScreenshot && modelName != "human-expert"
if (!isMediaProjectionPermissionGranted && requiresScreenCapturePermission) {
mainActivity.requestMediaProjectionPermission {
// Ask for Termux only after screen capture permission is granted.
requestTermuxPermissionThenSend(mainActivity)
if (mainActivity == null) {
handleTermuxRunCommandPermissionDenied()
return@IconButton
}
Toast.makeText(context, "Requesting screen capture permission...", Toast.LENGTH_SHORT).show()
return@IconButton
}

requestTermuxPermissionThenSend(mainActivity)
},
enabled = isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending,
modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)
) {
Icon(
Icons.AutoMirrored.Filled.Send,
stringResource(R.string.action_go),
tint = if (isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending)
MaterialTheme.colorScheme.primary else Color.Gray,
)
// Human Expert uses its own MediaProjection for WebRTC, not ScreenCaptureService.
// Termux is still required for every model, including Human Expert, via the shared send path below.
val currentModel = com.google.ai.sample.GenerativeAiViewModelFactory.getCurrentModel()
val requiresScreenCapturePermission = currentModel.supportsScreenshot && modelName != "human-expert"
if (!isMediaProjectionPermissionGranted && requiresScreenCapturePermission) {
mainActivity.requestMediaProjectionPermission {
// Ask for Termux only after screen capture permission is granted.
requestTermuxPermissionThenSend(mainActivity)
}
Toast.makeText(context, "Requesting screen capture permission...", Toast.LENGTH_SHORT).show()
return@IconButton
}

requestTermuxPermissionThenSend(mainActivity)
},
enabled = isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending,
modifier = Modifier.padding(top = 4.dp)
) {
Icon(
Icons.AutoMirrored.Filled.Send,
stringResource(R.string.action_go),
tint = if (isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending)
MaterialTheme.colorScheme.primary else Color.Gray,
)
}
}
} // Closes Row
LazyRow(modifier = Modifier.padding(all = 8.dp)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.google.ai.sample.util

import android.content.Context

object TermuxExecutionModePreferences {
private const val PREF_NAME = "termux_execution_mode_prefs"
private const val KEY_EXECUTE_IN_BACKGROUND = "execute_in_background"

fun executeInBackground(context: Context): Boolean =
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getBoolean(KEY_EXECUTE_IN_BACKGROUND, false)

fun setExecuteInBackground(context: Context, executeInBackground: Boolean) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_EXECUTE_IN_BACKGROUND, executeInBackground)
.apply()
}
}
Loading