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 d05dbfa..5098fd3 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 7fbc113..2e86557 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,42 +180,39 @@ 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 + fun sendCurrentQuestion() { + if (userQuestion.isNotBlank()) { + onReasonClicked(userQuestion, imageUris.toList()) + onUserQuestionChanged("") + imageUris.clear() + } + } - 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 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) } } - val termuxPermissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - TermuxFeedbackPreferences.resetPermissionDenialCount(context) - if (userQuestion.isNotBlank()) { - onReasonClicked(userQuestion, imageUris.toList()) - onUserQuestionChanged("") - imageUris.clear() + + fun requestTermuxPermissionThenSend(mainActivity: MainActivity) { + isTermuxPermissionRequestPending = true + mainActivity.requestTermuxRunCommandPermission { isGranted -> + isTermuxPermissionRequestPending = false + if (isGranted) { + TermuxFeedbackPreferences.resetPermissionDenialCount(context) + sendCurrentQuestion() + } else { + handleTermuxRunCommandPermissionDenied() } } } @@ -458,55 +451,36 @@ 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()) { - 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) - } else { - termuxPermissionLauncher.launch("com.termux.permission.RUN_COMMAND") - } - return@IconButton - } - TermuxFeedbackPreferences.resetPermissionDenialCount(context) - onReasonClicked(userQuestion, imageUris.toList()) - onUserQuestionChanged("") - imageUris.clear() - } + requestTermuxPermissionThenSend(mainActivity) }, - 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, ) }