Skip to content
Closed
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
31 changes: 31 additions & 0 deletions app/src/main/kotlin/com/google/ai/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -157,6 +159,33 @@ class MainActivity : ComponentActivity() {
private lateinit var requestForegroundServicePermissionLauncher: ActivityResultLauncher<String>
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<out String>,
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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -163,6 +158,7 @@ fun PhotoReasoningScreen(
var systemMessageEntries by rememberSaveable { mutableStateOf(emptyList<SystemMessageEntry>()) }
val focusManager = LocalFocusManager.current
val messages by chatMessages.collectAsState()
var isTermuxPermissionRequestPending by rememberSaveable { mutableStateOf(false) }

LaunchedEffect(Unit) {
systemMessageEntries = SystemMessageEntryPreferences.loadEntries(context)
Expand All @@ -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()
}
}
}
Expand Down Expand Up @@ -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,
)
}
Expand Down
Loading