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 @@ -424,12 +424,21 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
val delayMillis = pendingScreenshotDelayMillis
pendingScreenshotDelayMillis = 0L

fun buildScreenInfoPayload(rawScreenInfo: String?): String? {
val termuxOutput = TermuxOutputPreferences.consumeOutput(applicationContext)?.trim().orEmpty()
if (termuxOutput.isBlank()) {
return rawScreenInfo
}
Log.i(TAG, "executeTakeScreenshotCommand: Overriding Screen elements payload with Termux output. chars=${termuxOutput.length}")
return "Termux output:\n$termuxOutput"
}

val captureAndRequestScreenshot = {
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
if (!currentModel.supportsScreenshot) {
Log.d(TAG, "Command.TakeScreenshot: Model has no screenshot support, capturing screen info only.")
showToast("Capturing screen info...", false)
val screenInfo = captureScreenInformation()
val screenInfo = buildScreenInfoPayload(captureScreenInformation())
val mainActivity = MainActivity.getInstance()
mainActivity?.getPhotoReasoningViewModel()?.addScreenshotToConversation(
Uri.EMPTY,
Expand All @@ -440,7 +449,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.")
showToast("Preparing screenshot...", false)

val screenInfo = captureScreenInformation()
val screenInfo = buildScreenInfoPayload(captureScreenInformation())

val intent = Intent(MainActivity.ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT).apply {
putExtra(MainActivity.EXTRA_SCREEN_INFO, screenInfo)
Expand Down Expand Up @@ -632,31 +641,53 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
}
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")
?: resultBundle.getString("stdout")
?: ""
val stderr = resultBundle.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR")
?: resultBundle.getString("stderr")
?: ""
val extras = intent.extras
val stdout = sequenceOf(
resultBundle?.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT"),
resultBundle?.getString("stdout"),
extras?.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT"),
extras?.getString("stdout")
).firstOrNull { !it.isNullOrBlank() }.orEmpty()
val stderr = sequenceOf(
resultBundle?.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR"),
resultBundle?.getString("stderr"),
extras?.getString("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR"),
extras?.getString("stderr")
).firstOrNull { !it.isNullOrBlank() }.orEmpty()
val exitCode = when {
resultBundle.containsKey("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE") -> {
resultBundle?.containsKey("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE") == true -> {
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)
resultBundle?.containsKey("exitCode") == true -> resultBundle.getInt("exitCode", Int.MIN_VALUE)
extras?.containsKey("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE") == true -> {
extras.getInt("com.termux.app.extra.TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE", Int.MIN_VALUE)
}
extras?.containsKey("exitCode") == true -> extras.getInt("exitCode", Int.MIN_VALUE)
else -> Int.MIN_VALUE
}

Log.i(TAG, "Termux result received: exitCode=$exitCode stdoutLen=${stdout.length} stderrLen=${stderr.length} keys=${resultBundle.keySet().joinToString()}")
val resultKeys = resultBundle?.keySet()?.joinToString().orEmpty()
val extraKeys = extras?.keySet()?.joinToString().orEmpty()
Log.i(TAG, "Termux result received: exitCode=$exitCode stdoutLen=${stdout.length} stderrLen=${stderr.length} bundleKeys=$resultKeys extraKeys=$extraKeys")

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.")
val rawExtrasDump = extras?.keySet()?.joinToString("\n") { key -> "$key=${extras.get(key)}" }.orEmpty().trim()
if (rawExtrasDump.isBlank()) {
Log.w(TAG, "Ignoring Termux callback without stdout/stderr/exitCode and no readable extras.")
unregisterSelf()
return
}
Log.w(TAG, "Termux callback missing standard stdout/stderr/exitCode fields; falling back to raw extras dump for AI handoff.")
TermuxOutputPreferences.appendOutput(appContext, "Termux callback raw extras:\n$rawExtrasDump")
mainHandler.post {
MainActivity.getInstance()?.updateStatusMessage("Termux raw result captured", false)
}
serviceInstance?.handler?.post {
Log.d(TAG, "Termux raw callback captured, scheduling next command processing.")
serviceInstance?.scheduleNextCommandProcessing()
}
unregisterSelf()
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2663,10 +2663,11 @@ private fun processCommands(text: String) {
}
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}")
Log.i(TAG, "buildEnrichedScreenInfo: Replacing screen-elements bubble with Termux output. chars=${termuxOutputInfo.length}")
return termuxOutputInfo
}
val missingInfo = listOfNotNull(appNotFoundInfo, termuxNotFoundInfo).joinToString("\n").ifBlank { null }
val extraInfo = listOfNotNull(missingInfo, retrievedInfo, termuxOutputInfo).joinToString("\n\n").ifBlank { null }
val extraInfo = listOfNotNull(missingInfo, retrievedInfo).joinToString("\n\n").ifBlank { null }

return when {
!extraInfo.isNullOrBlank() && !screenInfo.isNullOrBlank() -> "$extraInfo\n\n$screenInfo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ object TermuxOutputPreferences {
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()
val committed = prefs.edit().putString(KEY_PENDING_OUTPUT, merged).commit()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: The synchronous commit() call performs blocking I/O on the calling thread. If appendOutput is called from the main thread, this will freeze the UI and may trigger ANR (Application Not Responding) dialogs, degrading user experience. Consider using a background thread with commit() or keeping apply() with proper synchronization mechanisms to avoid the race condition without blocking the UI thread.

if (!committed) {
throw IllegalStateException("Failed to persist pending Termux output")
}
Comment on lines +15 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Throwing IllegalStateException on commit failure will crash the app. commit() can legitimately return false in non-exceptional scenarios (disk full, storage permissions issues, device shutting down). This replaces a silent data loss issue with guaranteed app crashes. Consider logging the error and returning a failure indicator instead, or using an alternative synchronization strategy that doesn't crash on storage failures.

}

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()
val committed = prefs.edit().remove(KEY_PENDING_OUTPUT).commit()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: The synchronous commit() call performs blocking I/O on the calling thread. If consumeOutput is called from the main thread, this will freeze the UI and may trigger ANR (Application Not Responding) dialogs. Consider using a background thread with commit() or an alternative approach that doesn't block the UI thread.

if (!committed) {
throw IllegalStateException("Failed to clear consumed Termux output")
}
Comment on lines +25 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Throwing IllegalStateException on commit failure will crash the app instead of handling storage errors gracefully. commit() can return false in legitimate scenarios like disk full or permissions issues. This trades silent data loss for guaranteed crashes. Consider logging the error and handling it gracefully, or returning a failure indicator to the caller.

return value
}
}
Loading