From 8be6c3853ffe6fc1b12aee61dc29216ce5e558f2 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Wed, 13 May 2026 12:38:24 +0200 Subject: [PATCH 01/10] Use foreground current Termux session for commands --- .../google/ai/sample/ScreenOperatorAccessibilityService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index 0daa352..5c31823 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -594,8 +594,8 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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", true) - putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0) + putExtra("com.termux.RUN_COMMAND_BACKGROUND", false) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 1) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) From c6329126594ac173ebffe48ac16b948853f8618c Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Wed, 13 May 2026 23:02:55 +0200 Subject: [PATCH 02/10] Harden Termux permission gating and screenshot handoff --- .../ScreenOperatorAccessibilityService.kt | 32 +++++++++++++++++-- .../sample/util/TermuxFeedbackPreferences.kt | 15 +++++++++ .../ai/sample/util/TermuxOutputPreferences.kt | 6 ++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index 5c31823..a50b686 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -145,6 +145,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { private val handler = Handler(Looper.getMainLooper()) // Instance handler private var pendingScreenshotDelayMillis: Long = 0L + private var sawNonTermuxCommandSinceLastScreenshot: Boolean = false private var pendingDelayedScreenshotRunnable: Runnable? = null // App name to package mapper @@ -418,14 +419,35 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } } + .also { _ -> + if (command !is Command.TakeScreenshot && command !is Command.TermuxCommand) { + sawNonTermuxCommandSinceLastScreenshot = true + } + } } private fun executeTakeScreenshotCommand(): Boolean { val delayMillis = pendingScreenshotDelayMillis pendingScreenshotDelayMillis = 0L + val onlyTermuxContext = !sawNonTermuxCommandSinceLastScreenshot + + if (!isTermuxRunCommandPermissionGranted()) { + val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(applicationContext) + if (denialCount >= 2) { + showToast("Enable Termux permissions in the Android settings", true) + } + Log.w(TAG, "Blocking screenshot/AI handoff because Termux RUN_COMMAND permission is not granted.") + return false + } else { + TermuxFeedbackPreferences.resetPermissionDenialCount(applicationContext) + } fun buildScreenInfoPayload(rawScreenInfo: String?): String? { - val termuxOutput = TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty() + val termuxOutput = if (onlyTermuxContext) { + TermuxOutputPreferences.peekOutput(applicationContext)?.trim().orEmpty() + } else { + TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty() + } if (termuxOutput.isBlank()) { return rawScreenInfo } @@ -435,7 +457,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { val captureAndRequestScreenshot = { val currentModel = GenerativeAiViewModelFactory.getCurrentModel() - if (!currentModel.supportsScreenshot) { + if (!currentModel.supportsScreenshot || onlyTermuxContext) { Log.d(TAG, "Command.TakeScreenshot: Model has no screenshot support, capturing screen info only.") showToast("Capturing screen info...", false) val screenInfo = buildScreenInfoPayload(captureScreenInformation()) @@ -445,6 +467,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { applicationContext, screenInfo ) + sawNonTermuxCommandSinceLastScreenshot = false } else { Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.") showToast("Preparing screenshot...", false) @@ -457,6 +480,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } applicationContext.sendBroadcast(intent) Log.d(TAG, "Sent broadcast ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT to MainActivity with screenInfo.") + sawNonTermuxCommandSinceLastScreenshot = false } } @@ -477,6 +501,10 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { return true } + private fun isTermuxRunCommandPermissionGranted(): Boolean { + return checkSelfPermission("com.termux.permission.RUN_COMMAND") == PackageManager.PERMISSION_GRANTED + } + private fun cancelPendingDelayedScreenshot() { pendingScreenshotDelayMillis = 0L pendingDelayedScreenshotRunnable?.let { runnable -> diff --git a/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt b/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt index c9b1274..3271bd0 100644 --- a/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt +++ b/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt @@ -5,6 +5,7 @@ import android.content.Context object TermuxFeedbackPreferences { private const val PREF_NAME = "termux_feedback_prefs" private const val KEY_TERMUX_NOT_FOUND = "termux_not_found" + private const val KEY_TERMUX_PERMISSION_DENIAL_COUNT = "termux_permission_denial_count" fun markTermuxNotFound(context: Context) { context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) @@ -21,4 +22,18 @@ object TermuxFeedbackPreferences { } return value } + + fun incrementPermissionDenialCount(context: Context): Int { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val updated = prefs.getInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, 0) + 1 + prefs.edit().putInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, updated).apply() + return updated + } + + fun resetPermissionDenialCount(context: Context) { + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, 0) + .apply() + } } diff --git a/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt b/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt index 8ee182a..3c267ef 100644 --- a/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt +++ b/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt @@ -27,4 +27,10 @@ object TermuxOutputPreferences { } return value } + + fun peekOutput(context: Context): String? { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val value = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty().trim() + return value.ifBlank { null } + } } From 7de5fdd0ffe74f0fcdf3d1ef8aaed52b1ec65bbb Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 12:54:16 +0200 Subject: [PATCH 03/10] Move Termux permission gate to send action and revert regression --- .../ScreenOperatorAccessibilityService.kt | 37 ++----------------- .../multimodal/PhotoReasoningScreen.kt | 24 ++++++++++++ .../ai/sample/util/TermuxOutputPreferences.kt | 6 --- 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index a50b686..d8bf461 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -145,7 +145,6 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { private val handler = Handler(Looper.getMainLooper()) // Instance handler private var pendingScreenshotDelayMillis: Long = 0L - private var sawNonTermuxCommandSinceLastScreenshot: Boolean = false private var pendingDelayedScreenshotRunnable: Runnable? = null // App name to package mapper @@ -419,35 +418,13 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } } - .also { _ -> - if (command !is Command.TakeScreenshot && command !is Command.TermuxCommand) { - sawNonTermuxCommandSinceLastScreenshot = true - } - } } private fun executeTakeScreenshotCommand(): Boolean { val delayMillis = pendingScreenshotDelayMillis pendingScreenshotDelayMillis = 0L - val onlyTermuxContext = !sawNonTermuxCommandSinceLastScreenshot - - if (!isTermuxRunCommandPermissionGranted()) { - val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(applicationContext) - if (denialCount >= 2) { - showToast("Enable Termux permissions in the Android settings", true) - } - Log.w(TAG, "Blocking screenshot/AI handoff because Termux RUN_COMMAND permission is not granted.") - return false - } else { - TermuxFeedbackPreferences.resetPermissionDenialCount(applicationContext) - } - fun buildScreenInfoPayload(rawScreenInfo: String?): String? { - val termuxOutput = if (onlyTermuxContext) { - TermuxOutputPreferences.peekOutput(applicationContext)?.trim().orEmpty() - } else { - TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty() - } + val termuxOutput = TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty() if (termuxOutput.isBlank()) { return rawScreenInfo } @@ -457,7 +434,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { val captureAndRequestScreenshot = { val currentModel = GenerativeAiViewModelFactory.getCurrentModel() - if (!currentModel.supportsScreenshot || onlyTermuxContext) { + if (!currentModel.supportsScreenshot) { Log.d(TAG, "Command.TakeScreenshot: Model has no screenshot support, capturing screen info only.") showToast("Capturing screen info...", false) val screenInfo = buildScreenInfoPayload(captureScreenInformation()) @@ -467,7 +444,6 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { applicationContext, screenInfo ) - sawNonTermuxCommandSinceLastScreenshot = false } else { Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.") showToast("Preparing screenshot...", false) @@ -480,7 +456,6 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } applicationContext.sendBroadcast(intent) Log.d(TAG, "Sent broadcast ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT to MainActivity with screenInfo.") - sawNonTermuxCommandSinceLastScreenshot = false } } @@ -501,10 +476,6 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { return true } - private fun isTermuxRunCommandPermissionGranted(): Boolean { - return checkSelfPermission("com.termux.permission.RUN_COMMAND") == PackageManager.PERMISSION_GRANTED - } - private fun cancelPendingDelayedScreenshot() { pendingScreenshotDelayMillis = 0L pendingDelayedScreenshotRunnable?.let { runnable -> @@ -622,8 +593,8 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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_SESSION_ACTION", 1) + putExtra("com.termux.RUN_COMMAND_BACKGROUND", true) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index abb9985..3bbeb3c 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -6,6 +6,7 @@ import android.graphics.drawable.BitmapDrawable import android.net.Uri import android.provider.Settings import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -110,6 +111,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.TermuxFeedbackPreferences import com.google.ai.sample.util.UriSaver import com.google.ai.sample.util.shareTextFile import kotlinx.coroutines.Dispatchers @@ -425,6 +427,28 @@ fun PhotoReasoningScreen( } if (userQuestion.isNotBlank()) { + val hasTermuxRunCommandPermission = ContextCompat.checkSelfPermission( + context, + "com.termux.permission.RUN_COMMAND" + ) == android.content.pm.PackageManager.PERMISSION_GRANTED + if (!hasTermuxRunCommandPermission) { + val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(context) + if (denialCount >= 2) { + Toast.makeText( + context, + "Enable Termux permissions in the Android settings", + Toast.LENGTH_LONG + ).show() + } else { + Toast.makeText( + context, + "Please enable Termux run command permissions", + Toast.LENGTH_LONG + ).show() + } + return@IconButton + } + TermuxFeedbackPreferences.resetPermissionDenialCount(context) onReasonClicked(userQuestion, imageUris.toList()) onUserQuestionChanged("") imageUris.clear() diff --git a/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt b/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt index 3c267ef..8ee182a 100644 --- a/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt +++ b/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt @@ -27,10 +27,4 @@ object TermuxOutputPreferences { } return value } - - fun peekOutput(context: Context): String? { - val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - val value = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty().trim() - return value.ifBlank { null } - } } From 846d670c4407344d0c7750c1b7e381feba148e9c Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 14:25:48 +0200 Subject: [PATCH 04/10] Restore visible Termux session mode and refine send permission UX --- .../sample/ScreenOperatorAccessibilityService.kt | 14 +++++++++++--- .../feature/multimodal/PhotoReasoningScreen.kt | 9 +++++++-- .../ai/sample/util/TermuxFeedbackPreferences.kt | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index d8bf461..f616673 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -146,6 +146,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { private var pendingScreenshotDelayMillis: Long = 0L private var pendingDelayedScreenshotRunnable: Runnable? = null + private var sawNonTermuxCommandSinceLastScreenshot: Boolean = false // App name to package mapper private lateinit var appNamePackageMapper: AppNamePackageMapper @@ -417,12 +418,17 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { pressEnterKey() } } + }.also { + if (command !is Command.TakeScreenshot && command !is Command.TermuxCommand) { + sawNonTermuxCommandSinceLastScreenshot = true + } } } private fun executeTakeScreenshotCommand(): Boolean { val delayMillis = pendingScreenshotDelayMillis pendingScreenshotDelayMillis = 0L + val onlyTermuxContext = !sawNonTermuxCommandSinceLastScreenshot fun buildScreenInfoPayload(rawScreenInfo: String?): String? { val termuxOutput = TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty() if (termuxOutput.isBlank()) { @@ -434,7 +440,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { val captureAndRequestScreenshot = { val currentModel = GenerativeAiViewModelFactory.getCurrentModel() - if (!currentModel.supportsScreenshot) { + if (!currentModel.supportsScreenshot || onlyTermuxContext) { Log.d(TAG, "Command.TakeScreenshot: Model has no screenshot support, capturing screen info only.") showToast("Capturing screen info...", false) val screenInfo = buildScreenInfoPayload(captureScreenInformation()) @@ -444,6 +450,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { applicationContext, screenInfo ) + sawNonTermuxCommandSinceLastScreenshot = false } else { Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.") showToast("Preparing screenshot...", false) @@ -456,6 +463,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } applicationContext.sendBroadcast(intent) Log.d(TAG, "Sent broadcast ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT to MainActivity with screenInfo.") + sawNonTermuxCommandSinceLastScreenshot = false } } @@ -593,8 +601,8 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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", true) - putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0) + putExtra("com.termux.RUN_COMMAND_BACKGROUND", false) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 1) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index 3bbeb3c..b414132 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -2,8 +2,8 @@ package com.google.ai.sample.feature.multimodal import android.app.Activity import android.content.Intent -import android.graphics.drawable.BitmapDrawable import android.net.Uri +import android.graphics.drawable.BitmapDrawable import android.provider.Settings import android.widget.Toast import androidx.core.content.ContextCompat @@ -433,12 +433,17 @@ fun PhotoReasoningScreen( ) == android.content.pm.PackageManager.PERMISSION_GRANTED if (!hasTermuxRunCommandPermission) { val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(context) - if (denialCount >= 2) { + if (denialCount >= 3) { Toast.makeText( context, "Enable Termux permissions in the Android settings", Toast.LENGTH_LONG ).show() + val appInfoIntent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ) + context.startActivity(appInfoIntent) } else { Toast.makeText( context, diff --git a/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt b/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt index 3271bd0..6b1fb1e 100644 --- a/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt +++ b/app/src/main/kotlin/com/google/ai/sample/util/TermuxFeedbackPreferences.kt @@ -25,7 +25,7 @@ object TermuxFeedbackPreferences { fun incrementPermissionDenialCount(context: Context): Int { val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - val updated = prefs.getInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, 0) + 1 + val updated = (prefs.getInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, 0) + 1).coerceAtMost(3) prefs.edit().putInt(KEY_TERMUX_PERMISSION_DENIAL_COUNT, updated).apply() return updated } From 5dd74fd96146b333c62d41f30684d356b0569cf0 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 14:26:12 +0200 Subject: [PATCH 05/10] Use Android permission popup for Termux on first two send attempts --- .../feature/multimodal/PhotoReasoningScreen.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index b414132..637cb46 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -179,6 +179,18 @@ fun PhotoReasoningScreen( ) { uri -> uri?.let { imageUris.add(it) } } + val termuxPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + TermuxFeedbackPreferences.resetPermissionDenialCount(context) + if (userQuestion.isNotBlank()) { + onReasonClicked(userQuestion, imageUris.toList()) + onUserQuestionChanged("") + imageUris.clear() + } + } + } LaunchedEffect(messages.size, commandExecutionStatus, detectedCommands.size) { val chatMessageCount = messages.size @@ -445,11 +457,7 @@ fun PhotoReasoningScreen( ) context.startActivity(appInfoIntent) } else { - Toast.makeText( - context, - "Please enable Termux run command permissions", - Toast.LENGTH_LONG - ).show() + termuxPermissionLauncher.launch("com.termux.permission.RUN_COMMAND") } return@IconButton } From c66cd48404d227db7590158eef746aeb1a77947b Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 16:31:30 +0200 Subject: [PATCH 06/10] Change RUN_COMMAND_SESSION_ACTION value from 1 to 9 --- .../com/google/ai/sample/ScreenOperatorAccessibilityService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index f616673..4cfc578 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -602,7 +602,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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_SESSION_ACTION", 1) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 9) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) From 52ed2d1d8b4b4a5d633f6ef3a62881b0e3a1879b Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 16:43:12 +0200 Subject: [PATCH 07/10] Change RUN_COMMAND_SESSION_ACTION value from 9 to 3 --- .../com/google/ai/sample/ScreenOperatorAccessibilityService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index 4cfc578..9a000da 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -602,7 +602,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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_SESSION_ACTION", 9) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 3) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) From 23858e23d82e34e307de4394a64ce79ca259fece Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Sun, 17 May 2026 22:11:30 +0200 Subject: [PATCH 08/10] Update ScreenOperatorAccessibilityService.kt --- .../com/google/ai/sample/ScreenOperatorAccessibilityService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt index 9a000da..ea774d7 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -602,7 +602,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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_SESSION_ACTION", 3) + putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0) putExtra("com.termux.RUN_COMMAND_RUNNER", "app-shell") putExtra("com.termux.RUN_COMMAND_PENDING_INTENT", pendingResultIntent) putExtra("com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL", 0) From 555255fd917534593c013e97067018804772942d Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 19 May 2026 11:20:58 +0200 Subject: [PATCH 09/10] Clarify compilation instructions for code changes Updated instructions for compiling code changes to specify the use of './gradlew :app:compileDebugKotlin' and to omit the lint check. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c4e4171..2e644be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,6 @@ MOST IMPORTANT: 9. If debug compilation fails in your environment, resolve the issue before reporting it as complete. -10. For code changes only, compile only the code and do not perform a full build. +10. For code changes only, compile only the code with ./gradlew :app:compileDebugKotlin and omit always the lint check. 11. This app is production software and not a toy. From 16728b2cc2fb0923b1a9421e0d688e0710d5251d Mon Sep 17 00:00:00 2001 From: "amazon-q-developer[bot]" <208079219+amazon-q-developer[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 10:01:05 +0000 Subject: [PATCH 10/10] [skip ci] Fix "New" button to properly clear AI context by clearing command queue and Termux output --- .../ai/sample/feature/multimodal/PhotoReasoningScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt index 637cb46..ebf09ff 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreen.kt @@ -112,6 +112,7 @@ 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.TermuxFeedbackPreferences +import com.google.ai.sample.util.TermuxOutputPreferences import com.google.ai.sample.util.UriSaver import com.google.ai.sample.util.shareTextFile import kotlinx.coroutines.Dispatchers @@ -400,7 +401,11 @@ fun PhotoReasoningScreen( IconButton(onClick = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) }, modifier = Modifier.padding(bottom = 4.dp)) { Icon(Icons.Rounded.Add, stringResource(R.string.add_image)) } - IconButton(onClick = onClearChatHistory, modifier = Modifier.padding(top = 4.dp).drawBehind { + IconButton(onClick = { + ScreenOperatorAccessibilityService.clearCommandQueue() + TermuxOutputPreferences.consumeOutput(context) + onClearChatHistory() + }, modifier = Modifier.padding(top = 4.dp).drawBehind { drawCircle(color = Color.Black, radius = size.minDimension / 2, style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx())) }) { Text("New", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) } }