From 935e323fae097822b1a7f7d9ead197188a15093a Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 09:57:30 +0200 Subject: [PATCH 1/8] Fix Termux RUN_COMMAND dispatch and add detailed diagnostics --- .../ScreenOperatorAccessibilityService.kt | 44 ++++++++++++++++++- 1 file changed, 42 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 17374ff..3e7110c 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -529,6 +529,13 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } private fun executeTermuxCommand(command: String) { + Log.i(TAG, "Termux command requested. Raw command length=${command.length}") + val trimmedCommand = command.trim() + if (trimmedCommand.isEmpty()) { + Log.w(TAG, "Skipping Termux command dispatch because command is empty after trim.") + return + } + val termuxPackage = "com.termux" val pm = packageManager val launchIntent = pm.getLaunchIntentForPackage(termuxPackage) @@ -537,17 +544,50 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { Log.w(TAG, "Termux not found for command execution.") return } + + val runCommandServiceClass = "com.termux.app.RunCommandService" + val serviceProbeIntent = Intent("com.termux.RUN_COMMAND").apply { + `package` = termuxPackage + setClassName(termuxPackage, runCommandServiceClass) + } + val resolvedService = pm.resolveService(serviceProbeIntent, PackageManager.MATCH_DEFAULT_ONLY) + if (resolvedService == null) { + Log.e(TAG, "Termux RunCommandService not resolvable. package=$termuxPackage class=$runCommandServiceClass") + TermuxFeedbackPreferences.markTermuxNotFound(applicationContext) + return + } + + Log.i( + TAG, + "Resolved Termux RunCommandService=${resolvedService.serviceInfo?.name}, app=${resolvedService.serviceInfo?.packageName}" + ) + val intent = Intent("com.termux.RUN_COMMAND").apply { `package` = termuxPackage - setClassName(termuxPackage, "com.termux.app.RunCommandService") + 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", command)) + 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_RUNNER", "app-shell") } + + Log.i( + TAG, + "Dispatching Termux RUN_COMMAND with path=${intent.getStringExtra("com.termux.RUN_COMMAND_PATH")}, " + + "workdir=${intent.getStringExtra("com.termux.RUN_COMMAND_WORKDIR")}, " + + "background=${intent.getBooleanExtra("com.termux.RUN_COMMAND_BACKGROUND", false)}, " + + "runner=${intent.getStringExtra("com.termux.RUN_COMMAND_RUNNER")}, " + + "argsCount=${intent.getStringArrayExtra("com.termux.RUN_COMMAND_ARGUMENTS")?.size ?: 0}" + ) + try { startService(intent) + Log.i(TAG, "Termux command dispatch succeeded.") + } catch (se: SecurityException) { + Log.e(TAG, "Failed to dispatch Termux command due to security restriction. Check Termux RUN_COMMAND permission grant.", se) + TermuxFeedbackPreferences.markTermuxNotFound(applicationContext) } catch (t: Throwable) { Log.e(TAG, "Failed to dispatch Termux command", t) TermuxFeedbackPreferences.markTermuxNotFound(applicationContext) From a35ead065ca9dfb893d22424b76688bf8cc81c77 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 15:50:16 +0200 Subject: [PATCH 2/8] Add Termux result callback handling and output status streaming --- .../ScreenOperatorAccessibilityService.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) 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 3e7110c..30dcf65 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -7,6 +7,7 @@ import android.accessibilityservice.GestureDescription import android.content.ComponentName import android.content.Context import android.content.Intent +import android.app.PendingIntent import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.graphics.Bitmap @@ -562,6 +563,21 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { "Resolved Termux RunCommandService=${resolvedService.serviceInfo?.name}, app=${resolvedService.serviceInfo?.packageName}" ) + val callbackAction = "com.google.ai.sample.TERMUX_COMMAND_RESULT" + val callbackIntent = Intent(callbackAction).apply { + `package` = packageName + } + val callbackFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + val pendingResultIntent = PendingIntent.getBroadcast(applicationContext, 7001, callbackIntent, callbackFlags) + + val callbackReceiver = TermuxResultReceiver(applicationContext) + try { + registerReceiver(callbackReceiver, android.content.IntentFilter(callbackAction), Context.RECEIVER_NOT_EXPORTED) + Log.i(TAG, "Registered Termux result receiver for action=$callbackAction") + } catch (t: Throwable) { + Log.e(TAG, "Failed to register Termux result receiver", t) + } + val intent = Intent("com.termux.RUN_COMMAND").apply { `package` = termuxPackage setClassName(termuxPackage, runCommandServiceClass) @@ -571,6 +587,10 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { 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) + putExtra("com.termux.RUN_COMMAND_RETURN_STDOUT", true) + putExtra("com.termux.RUN_COMMAND_RETURN_STDERR", true) } Log.i( @@ -594,6 +614,58 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } + private class TermuxResultReceiver(private val appContext: Context) : android.content.BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) { + Log.w(TAG, "Termux result receiver invoked with null intent") + return + } + val resultBundle = intent.getBundleExtra("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE") + if (resultBundle == null) { + Log.w(TAG, "Termux result bundle missing; available extras=${intent.extras?.keySet()?.joinToString()}") + return + } + + val stdout = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT", "") ?: "" + val stderr = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR", "") ?: "" + val exitCode = resultBundle.getInt("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE", Int.MIN_VALUE) + + Log.i(TAG, "Termux result received: exitCode=$exitCode stdoutLen=${stdout.length} stderrLen=${stderr.length}") + + val combined = buildString { + append("Termux finished (exit=") + append(exitCode) + append(")") + if (stdout.isNotBlank()) { + append("\nstdout:\n") + append(stdout) + } + if (stderr.isNotBlank()) { + append("\nstderr:\n") + append(stderr) + } + } + + mainHandler.post { + MainActivity.getInstance()?.updateStatusMessage("Termux stream start", false) + } + combined.lineSequence().forEachIndexed { idx, line -> + val framed = "Termux[$idx]: $line" + Log.d(TAG, framed) + mainHandler.post { + MainActivity.getInstance()?.updateStatusMessage(framed, false) + } + } + + try { + appContext.unregisterReceiver(this) + Log.i(TAG, "Termux result receiver unregistered") + } catch (t: Throwable) { + Log.w(TAG, "Failed to unregister Termux result receiver", t) + } + } + } + private fun processCommandQueue() { if (!commandQueue.tryAcquireProcessing()) { From e3c9298a89928b95b72cacecee7e1d0c145895a6 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 16:23:47 +0200 Subject: [PATCH 3/8] Fix Termux result parsing and carry output into next screenshot prompt --- .../ScreenOperatorAccessibilityService.kt | 6 +++++ .../multimodal/PhotoReasoningViewModel.kt | 1 + .../ai/sample/util/TermuxOutputPreferences.kt | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt 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 30dcf65..aa49cfa 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -30,6 +30,7 @@ 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.TermuxFeedbackPreferences +import com.google.ai.sample.util.TermuxOutputPreferences import java.io.File import java.text.SimpleDateFormat import com.google.ai.sample.GenerativeViewModelFactory @@ -621,6 +622,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { return } val resultBundle = intent.getBundleExtra("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE") + ?: intent.getBundleExtra("result") if (resultBundle == null) { Log.w(TAG, "Termux result bundle missing; available extras=${intent.extras?.keySet()?.joinToString()}") return @@ -646,6 +648,10 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } + val aiRelevantOutput = combined.trim() + TermuxOutputPreferences.appendOutput(appContext, aiRelevantOutput) + Log.i(TAG, "Stored Termux output for next screenshot bubble. chars=${aiRelevantOutput.length}") + mainHandler.post { MainActivity.getInstance()?.updateStatusMessage("Termux stream start", false) } diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt index 5ed8f1f..34c1e43 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt @@ -31,6 +31,7 @@ import com.google.ai.sample.util.CommandParser import com.google.ai.sample.util.SystemMessagePreferences import com.google.ai.sample.util.SystemMessageEntry import com.google.ai.sample.util.TermuxFeedbackPreferences +import com.google.ai.sample.util.TermuxOutputPreferences import com.google.ai.sample.util.UserInputPreferences import com.google.ai.sample.feature.multimodal.ModelDownloadManager import com.google.ai.sample.ModelOption 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 new file mode 100644 index 0000000..ec2d0ab --- /dev/null +++ b/app/src/main/kotlin/com/google/ai/sample/util/TermuxOutputPreferences.kt @@ -0,0 +1,24 @@ +package com.google.ai.sample.util + +import android.content.Context + +object TermuxOutputPreferences { + private const val PREF_NAME = "termux_output_prefs" + private const val KEY_PENDING_OUTPUT = "pending_output" + + fun appendOutput(context: Context, output: String) { + if (output.isBlank()) return + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val existing = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty() + val merged = if (existing.isBlank()) output else "$existing\n\n$output" + prefs.edit().putString(KEY_PENDING_OUTPUT, merged).apply() + } + + fun consumeOutput(context: Context): String? { + val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + val value = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty().trim() + if (value.isBlank()) return null + prefs.edit().remove(KEY_PENDING_OUTPUT).apply() + return value + } +} From e4aca4453a39ff2623d8b4a5f958a3c63ada74b7 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 16:46:32 +0200 Subject: [PATCH 4/8] Fix Termux callback key parsing and receiver lifecycle cleanup --- .../ScreenOperatorAccessibilityService.kt | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 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 aa49cfa..983c9ea 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -573,7 +573,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { val callbackReceiver = TermuxResultReceiver(applicationContext) try { - registerReceiver(callbackReceiver, android.content.IntentFilter(callbackAction), Context.RECEIVER_NOT_EXPORTED) + applicationContext.registerReceiver(callbackReceiver, android.content.IntentFilter(callbackAction), Context.RECEIVER_NOT_EXPORTED) Log.i(TAG, "Registered Termux result receiver for action=$callbackAction") } catch (t: Throwable) { Log.e(TAG, "Failed to register Termux result receiver", t) @@ -617,27 +617,57 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { private class TermuxResultReceiver(private val appContext: Context) : android.content.BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + fun unregisterSelf() { + try { + appContext.unregisterReceiver(this) + Log.i(TAG, "Termux result receiver unregistered") + } catch (t: Throwable) { + Log.w(TAG, "Failed to unregister Termux result receiver", t) + } + } if (intent == null) { Log.w(TAG, "Termux result receiver invoked with null intent") + unregisterSelf() return } val resultBundle = intent.getBundleExtra("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE") ?: intent.getBundleExtra("result") if (resultBundle == null) { Log.w(TAG, "Termux result bundle missing; available extras=${intent.extras?.keySet()?.joinToString()}") + unregisterSelf() return } - val stdout = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT", "") ?: "" - val stderr = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR", "") ?: "" - val exitCode = resultBundle.getInt("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE", Int.MIN_VALUE) + val stdout = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT") + ?: resultBundle.getString("stdout") + ?: "" + val stderr = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR") + ?: resultBundle.getString("stderr") + ?: "" + val exitCode = when { + resultBundle.containsKey("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE") -> { + resultBundle.getInt("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE", Int.MIN_VALUE) + } + resultBundle.containsKey("exitCode") -> resultBundle.getInt("exitCode", Int.MIN_VALUE) + else -> Int.MIN_VALUE + } - Log.i(TAG, "Termux result received: exitCode=$exitCode stdoutLen=${stdout.length} stderrLen=${stderr.length}") + Log.i(TAG, "Termux result received: exitCode=$exitCode stdoutLen=${stdout.length} stderrLen=${stderr.length} keys=${resultBundle.keySet().joinToString()}") + + val hasKnownResult = stdout.isNotBlank() || stderr.isNotBlank() || exitCode != Int.MIN_VALUE + if (!hasKnownResult) { + Log.w(TAG, "Ignoring Termux callback without stdout/stderr/exitCode to avoid polluting pending output.") + unregisterSelf() + return + } val combined = buildString { - append("Termux finished (exit=") - append(exitCode) - append(")") + append("Termux finished") + if (exitCode != Int.MIN_VALUE) { + append(" (exit=") + append(exitCode) + append(")") + } if (stdout.isNotBlank()) { append("\nstdout:\n") append(stdout) @@ -649,8 +679,10 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } val aiRelevantOutput = combined.trim() - TermuxOutputPreferences.appendOutput(appContext, aiRelevantOutput) - Log.i(TAG, "Stored Termux output for next screenshot bubble. chars=${aiRelevantOutput.length}") + if (aiRelevantOutput.isNotBlank()) { + TermuxOutputPreferences.appendOutput(appContext, aiRelevantOutput) + Log.i(TAG, "Stored Termux output for next screenshot bubble. chars=${aiRelevantOutput.length}") + } mainHandler.post { MainActivity.getInstance()?.updateStatusMessage("Termux stream start", false) @@ -663,12 +695,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } - try { - appContext.unregisterReceiver(this) - Log.i(TAG, "Termux result receiver unregistered") - } catch (t: Throwable) { - Log.w(TAG, "Failed to unregister Termux result receiver", t) - } + unregisterSelf() } } From 80e86c1f0c314b5bff760173d2b79c8efe7b5812 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 17:29:48 +0200 Subject: [PATCH 5/8] Make Termux commands async and resume queue on callback --- .../google/ai/sample/ScreenOperatorAccessibilityService.kt | 6 +++++- 1 file changed, 5 insertions(+), 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 983c9ea..fad120c 100644 --- a/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt +++ b/app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt @@ -386,7 +386,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } is Command.TermuxCommand -> { - executeSyncCommandAction( + executeAsyncCommandAction( logMessage = "Executing Termux command: ${command.command}", toastMessage = "Executing Termux command..." ) { @@ -695,6 +695,10 @@ class ScreenOperatorAccessibilityService : AccessibilityService() { } } + serviceInstance?.handler?.post { + Log.d(TAG, "Termux result received, scheduling next command processing.") + serviceInstance?.scheduleNextCommandProcessing() + } unregisterSelf() } } From ec55df9491db55931be67a477aec75e912836e1e Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 18:02:53 +0200 Subject: [PATCH 6/8] Inject Termux output into enriched screen info and cap output buffer --- .../feature/multimodal/PhotoReasoningViewModel.kt | 13 +++++++------ .../ai/sample/util/TermuxOutputPreferences.kt | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt index 34c1e43..e0f5e3f 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt @@ -2659,15 +2659,16 @@ private fun processCommands(text: String) { } else { null } + val termuxOutputInfo = TermuxOutputPreferences.consumeOutput(appContext)?.let { "Termux output:\n$it" } + if (!termuxOutputInfo.isNullOrBlank()) { + Log.i(TAG, "buildEnrichedScreenInfo: Injecting Termux output into next screen-info bubble. chars=${termuxOutputInfo.length}") + } val missingInfo = listOfNotNull(appNotFoundInfo, termuxNotFoundInfo).joinToString("\n").ifBlank { null } + val extraInfo = listOfNotNull(missingInfo, retrievedInfo, termuxOutputInfo).joinToString("\n\n").ifBlank { null } return when { - !missingInfo.isNullOrBlank() && !retrievedInfo.isNullOrBlank() && !screenInfo.isNullOrBlank() -> "$missingInfo\n\n$retrievedInfo\n\n$screenInfo" - !missingInfo.isNullOrBlank() && !retrievedInfo.isNullOrBlank() -> "$missingInfo\n\n$retrievedInfo" - !missingInfo.isNullOrBlank() && !screenInfo.isNullOrBlank() -> "$missingInfo\n\n$screenInfo" - !missingInfo.isNullOrBlank() -> missingInfo - !retrievedInfo.isNullOrBlank() && !screenInfo.isNullOrBlank() -> "$retrievedInfo\n\n$screenInfo" - !retrievedInfo.isNullOrBlank() -> retrievedInfo + !extraInfo.isNullOrBlank() && !screenInfo.isNullOrBlank() -> "$extraInfo\n\n$screenInfo" + !extraInfo.isNullOrBlank() -> extraInfo !screenInfo.isNullOrBlank() -> screenInfo else -> null } 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 ec2d0ab..ab39c0b 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 @@ -5,12 +5,18 @@ import android.content.Context object TermuxOutputPreferences { private const val PREF_NAME = "termux_output_prefs" private const val KEY_PENDING_OUTPUT = "pending_output" + private const val MAX_BUFFER_CHARS = 16000 fun appendOutput(context: Context, output: String) { if (output.isBlank()) return val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) val existing = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty() - val merged = if (existing.isBlank()) output else "$existing\n\n$output" + val mergedRaw = if (existing.isBlank()) output else "$existing\n\n$output" + val merged = if (mergedRaw.length > MAX_BUFFER_CHARS) { + mergedRaw.takeLast(MAX_BUFFER_CHARS) + } else { + mergedRaw + } prefs.edit().putString(KEY_PENDING_OUTPUT, merged).apply() } From df016a3119dfafc86afce024584060f6543d073b Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 18:03:19 +0200 Subject: [PATCH 7/8] Remove Termux output buffer cap per requirement --- .../com/google/ai/sample/util/TermuxOutputPreferences.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 ab39c0b..ec2d0ab 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 @@ -5,18 +5,12 @@ import android.content.Context object TermuxOutputPreferences { private const val PREF_NAME = "termux_output_prefs" private const val KEY_PENDING_OUTPUT = "pending_output" - private const val MAX_BUFFER_CHARS = 16000 fun appendOutput(context: Context, output: String) { if (output.isBlank()) return val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) val existing = prefs.getString(KEY_PENDING_OUTPUT, "").orEmpty() - val mergedRaw = if (existing.isBlank()) output else "$existing\n\n$output" - val merged = if (mergedRaw.length > MAX_BUFFER_CHARS) { - mergedRaw.takeLast(MAX_BUFFER_CHARS) - } else { - mergedRaw - } + val merged = if (existing.isBlank()) output else "$existing\n\n$output" prefs.edit().putString(KEY_PENDING_OUTPUT, merged).apply() } From a17f2e29872c55a7eb7bd12ef174c7b7b59ab94d Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Tue, 12 May 2026 18:39:30 +0200 Subject: [PATCH 8/8] Fix debouncer consuming Termux output before screenshot processing --- .../ai/sample/feature/multimodal/PhotoReasoningViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt index e0f5e3f..3393a9b 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt @@ -2559,9 +2559,8 @@ private fun processCommands(text: String) { context: Context, screenInfo: String? = null ) { - val enrichedScreenInfo = buildEnrichedScreenInfo(screenInfo) - if (screenshotUri == Uri.EMPTY) { + val enrichedScreenInfo = buildEnrichedScreenInfo(screenInfo) // This case is for offline models, where we don't have a screenshot. // We just want to send the screen info. val genericAnalysisPrompt = createGenericScreenshotPrompt() @@ -2579,6 +2578,9 @@ private fun processCommands(text: String) { return // Exit the function early if it's a duplicate call within the window } + val enrichedScreenInfo = buildEnrichedScreenInfo(screenInfo) + Log.d(TAG, "addScreenshotToConversation: Using enrichedScreenInfo=${!enrichedScreenInfo.isNullOrBlank()} for URI=$screenshotUri") + PhotoReasoningApplication.applicationScope.launch(Dispatchers.Main) { try { Log.d(TAG, "Adding screenshot to conversation: $screenshotUri")