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 1/2] 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 2/2] 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 } + } }