From 5ff58719429f9f704f53b130d2ef50499394ff24 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:20:21 +0200 Subject: [PATCH 1/2] Request Termux permission directly before sending --- .../com/google/ai/sample/MainActivity.kt | 31 ++++++ .../multimodal/PhotoReasoningScreen.kt | 96 +++++++------------ 2 files changed, 65 insertions(+), 62 deletions(-) diff --git a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt index d05dbfae..5098fd37 100644 --- a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt +++ b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt @@ -66,6 +66,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController @@ -129,6 +130,7 @@ class MainActivity : ComponentActivity() { private var isProcessingExplicitScreenshotRequest: Boolean = false private var onMediaProjectionPermissionGranted: (() -> Unit)? = null private var onWebRtcMediaProjectionResult: ((Int, Intent) -> Unit)? = null + private var onTermuxRunCommandPermissionResult: ((Boolean) -> Unit)? = null private val mediaProjectionServiceStarter by lazy { MediaProjectionServiceStarter(this) } // Payment dialog state @@ -157,6 +159,33 @@ class MainActivity : ComponentActivity() { private lateinit var requestForegroundServicePermissionLauncher: ActivityResultLauncher private val foregroundMediaProjectionPermission = android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION + + fun requestTermuxRunCommandPermission(onResult: (Boolean) -> Unit) { + Log.d(TAG, "Requesting Termux RUN_COMMAND permission without pre-check") + onTermuxRunCommandPermissionResult = onResult + ActivityCompat.requestPermissions( + this, + arrayOf(TERMUX_RUN_COMMAND_PERMISSION), + REQUEST_CODE_TERMUX_RUN_COMMAND_PERMISSION + ) + } + + @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == REQUEST_CODE_TERMUX_RUN_COMMAND_PERMISSION) { + val isGranted = grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED + Log.d(TAG, "Termux RUN_COMMAND permission result: $isGranted") + onTermuxRunCommandPermissionResult?.invoke(isGranted) + onTermuxRunCommandPermissionResult = null + } + } + fun requestMediaProjectionPermission(onGranted: (() -> Unit)? = null) { Log.d(TAG, "Requesting MediaProjection permission") onMediaProjectionPermissionGranted = onGranted @@ -1136,6 +1165,8 @@ class MainActivity : ComponentActivity() { } private const val PREFS_NAME = "AppPrefs" private const val PREF_KEY_FIRST_LAUNCH_INFO_SHOWN = "firstLaunchInfoShown" + private const val TERMUX_RUN_COMMAND_PERMISSION = "com.termux.permission.RUN_COMMAND" + private const val REQUEST_CODE_TERMUX_RUN_COMMAND_PERMISSION = 1001 // New Broadcast Actions for MediaProjection Screenshot Flow const val ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT = "com.google.ai.sample.REQUEST_MEDIAPROJECTION_SCREENSHOT" 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 7fbc1130..e41485c0 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 @@ -5,11 +5,7 @@ import android.content.Intent import android.net.Uri import android.graphics.drawable.BitmapDrawable import android.provider.Settings -import android.os.Build -import android.content.pm.PackageManager -import android.content.pm.PackageInfo import android.widget.Toast -import androidx.core.content.ContextCompat import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -125,7 +121,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json -import android.util.Log import kotlinx.serialization.SerializationException @Composable @@ -163,6 +158,7 @@ fun PhotoReasoningScreen( var systemMessageEntries by rememberSaveable { mutableStateOf(emptyList()) } val focusManager = LocalFocusManager.current val messages by chatMessages.collectAsState() + var isTermuxPermissionRequestPending by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { systemMessageEntries = SystemMessageEntryPreferences.loadEntries(context) @@ -184,43 +180,27 @@ fun PhotoReasoningScreen( uri?.let { imageUris.add(it) } } - fun hasTermuxRunCommandPermission(): Boolean { - val runtimeGranted = ContextCompat.checkSelfPermission( - context, - "com.termux.permission.RUN_COMMAND" - ) == PackageManager.PERMISSION_GRANTED - if (!runtimeGranted) return false - - return runCatching { - val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.getPackageInfo( - context.packageName, - PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()) - ) - } else { - @Suppress("DEPRECATION") - context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) - } - val permissions = packageInfo.requestedPermissions ?: return@runCatching runtimeGranted - val flags = packageInfo.requestedPermissionsFlags ?: return@runCatching runtimeGranted - val index = permissions.indexOf("com.termux.permission.RUN_COMMAND") - if (index == -1 || index >= flags.size) runtimeGranted - else (flags[index] and PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0 - }.getOrElse { - Log.w("PhotoReasoningScreen", "Unable to verify requestedPermissionsFlags for Termux permission", it) - runtimeGranted + fun sendCurrentQuestion() { + if (userQuestion.isNotBlank()) { + onReasonClicked(userQuestion, imageUris.toList()) + onUserQuestionChanged("") + imageUris.clear() } } - val termuxPermissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - TermuxFeedbackPreferences.resetPermissionDenialCount(context) - if (userQuestion.isNotBlank()) { - onReasonClicked(userQuestion, imageUris.toList()) - onUserQuestionChanged("") - imageUris.clear() - } + + fun handleTermuxRunCommandPermissionDenied() { + val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(context) + 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) } } @@ -475,38 +455,30 @@ fun PhotoReasoningScreen( } if (userQuestion.isNotBlank()) { - val hasTermuxRunCommandPermission = hasTermuxRunCommandPermission() - if (!hasTermuxRunCommandPermission) { - val denialCount = TermuxFeedbackPreferences.incrementPermissionDenialCount(context) - 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) + if (mainActivity == null) { + handleTermuxRunCommandPermissionDenied() + return@IconButton + } + + isTermuxPermissionRequestPending = true + mainActivity.requestTermuxRunCommandPermission { isGranted -> + isTermuxPermissionRequestPending = false + if (isGranted) { + TermuxFeedbackPreferences.resetPermissionDenialCount(context) + sendCurrentQuestion() } else { - termuxPermissionLauncher.launch("com.termux.permission.RUN_COMMAND") + handleTermuxRunCommandPermissionDenied() } - return@IconButton } - TermuxFeedbackPreferences.resetPermissionDenialCount(context) - onReasonClicked(userQuestion, imageUris.toList()) - onUserQuestionChanged("") - imageUris.clear() } }, - enabled = isInitialized && userQuestion.isNotBlank(), + 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()) + tint = if (isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending) MaterialTheme.colorScheme.primary else Color.Gray, ) } From 423cc8293210ba304ae16887fd5e9eebc20c9cd2 Mon Sep 17 00:00:00 2001 From: Android PowerUser <88908510+Android-PowerUser@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:18:22 +0200 Subject: [PATCH 2/2] Request Termux permission after MediaProjection grant --- .../multimodal/PhotoReasoningScreen.kt | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 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 e41485c0..2e86557b 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 @@ -204,6 +204,19 @@ fun PhotoReasoningScreen( } } + fun requestTermuxPermissionThenSend(mainActivity: MainActivity) { + isTermuxPermissionRequestPending = true + mainActivity.requestTermuxRunCommandPermission { isGranted -> + isTermuxPermissionRequestPending = false + if (isGranted) { + TermuxFeedbackPreferences.resetPermissionDenialCount(context) + sendCurrentQuestion() + } else { + handleTermuxRunCommandPermissionDenied() + } + } + } + LaunchedEffect(messages.size, commandExecutionStatus, detectedCommands.size) { val chatMessageCount = messages.size var targetIndex = -1 // Default to no scroll if no items @@ -438,39 +451,28 @@ fun PhotoReasoningScreen( return@IconButton } + if (userQuestion.isBlank()) { + return@IconButton + } + + if (mainActivity == null) { + handleTermuxRunCommandPermissionDenied() + return@IconButton + } + // Check MediaProjection only for models that support screenshots and are not human-expert. // Human Expert uses its own MediaProjection for WebRTC, not ScreenCaptureService. val currentModel = com.google.ai.sample.GenerativeAiViewModelFactory.getCurrentModel() if (!isMediaProjectionPermissionGranted && currentModel.supportsScreenshot && modelName != "human-expert") { - mainActivity?.requestMediaProjectionPermission { - // This block will be executed after permission is granted - if (userQuestion.isNotBlank()) { - onReasonClicked(userQuestion, imageUris.toList()) - onUserQuestionChanged("") - imageUris.clear() - } + 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 } - if (userQuestion.isNotBlank()) { - if (mainActivity == null) { - handleTermuxRunCommandPermissionDenied() - return@IconButton - } - - isTermuxPermissionRequestPending = true - mainActivity.requestTermuxRunCommandPermission { isGranted -> - isTermuxPermissionRequestPending = false - if (isGranted) { - TermuxFeedbackPreferences.resetPermissionDenialCount(context) - sendCurrentQuestion() - } else { - handleTermuxRunCommandPermissionDenied() - } - } - } + requestTermuxPermissionThenSend(mainActivity) }, enabled = isInitialized && userQuestion.isNotBlank() && !isTermuxPermissionRequestPending, modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)