Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0609066
feat: remote overrides for built-in model identifiers + offline model…
claude Jun 26, 2026
3e58714
Delete safety boundary section from documentation
Android-PowerUser Jun 27, 2026
85e3f9a
Remove comments from CommandType enum
Android-PowerUser Jun 27, 2026
6d766b1
Remove safety boundary notes from CommandPatternConfig
Android-PowerUser Jun 27, 2026
155c448
feat: allow new CommandTypes to be defined and updated via WebView
claude Jun 27, 2026
5480f15
Add remote-updatable execution policy (max commands per message + fee…
claude Jun 28, 2026
3c367b4
Expand WebView remote-override surface: app mappings + error classifi…
claude Jun 29, 2026
9c1d734
Add remote-updatable trial/donation dialog text (TrialUiConfig)
claude Jun 29, 2026
7f34a3c
Add remote-updatable operational tuning (Mistral/download retry timin…
claude Jun 29, 2026
c668b93
Make trial duration remote-updatable via trial-duration-overrides.json
claude Jun 29, 2026
40e0dcd
Make generation-settings factory defaults remote-updatable
claude Jun 29, 2026
15eaee3
Make the 'Retrieved information [...]' marker remote-updatable; docum…
claude Jun 30, 2026
4e4a3a7
Correct prior mistake: make the 'Screen elements:' marker remote-upda…
claude Jun 30, 2026
bde5bef
Make native (Compose-screen) UI strings remote-updatable via ui-strin…
claude Jun 30, 2026
164187d
Add a working toast("message") AI command, and the missing showToast …
claude Jun 30, 2026
6166748
Expose every remaining native device-control capability as a bridge m…
claude Jun 30, 2026
2a6cfd5
Add pinch()/PinchGesture as a first-class built-in command (zoom in/out)
claude Jul 1, 2026
2e9d682
Add clipboard copy/read to WebView JSON bridge (no extra permission n…
claude Jul 1, 2026
3ced4ca
Fix return type of executePinchGesture to return Boolean
Android-PowerUser Jul 1, 2026
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
1 change: 1 addition & 0 deletions app-mappings-overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
12 changes: 6 additions & 6 deletions app/src/main/kotlin/com/google/ai/sample/ApiKeyDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ fun ApiKeyDialog(
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Puter Link", url)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, "Link is in the clipboard.", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "After the sign up paste the link in the Browser", Toast.LENGTH_LONG).show()
Toast.makeText(context, com.google.ai.sample.util.UiStringsConfig.get("toast_link_copied", "Link is in the clipboard."), Toast.LENGTH_SHORT).show()
Toast.makeText(context, com.google.ai.sample.util.UiStringsConfig.get("toast_paste_link_in_browser", "After the sign up paste the link in the Browser"), Toast.LENGTH_LONG).show()
}
if (selectedProvider == ApiProvider.CLOUDFLARE) {
Toast.makeText(context, "After sign up use the search bar to find \"API token\"", Toast.LENGTH_LONG).show()
Toast.makeText(context, com.google.ai.sample.util.UiStringsConfig.get("toast_find_api_token", "After sign up use the search bar to find \"API token\""), Toast.LENGTH_LONG).show()
}

val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
Expand Down Expand Up @@ -171,7 +171,7 @@ fun ApiKeyDialog(
}
}
) {
Text("Add")
Text(com.google.ai.sample.util.UiStringsConfig.get("apikey_add_button", "Add"))
}
}

Expand Down Expand Up @@ -223,7 +223,7 @@ fun ApiKeyDialog(
loadKeysForProvider(selectedProvider)
}
) {
Text("✕", textAlign = TextAlign.Center)
Text(com.google.ai.sample.util.UiStringsConfig.get("apikey_close_icon", "\u2715"), textAlign = TextAlign.Center)
}
}
}
Expand All @@ -238,7 +238,7 @@ fun ApiKeyDialog(
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismiss) {
Text("Close")
Text(com.google.ai.sample.util.UiStringsConfig.get("apikey_close_button", "Close"))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
// Live API models
val liveApiManager = LiveApiManager(
apiKey = apiKey,
modelName = currentModel.modelName,
modelName = com.google.ai.sample.util.ModelIdentifierOverrides.resolve(currentModel),
temperature = genSettings.temperature.toDouble(),
topP = genSettings.topP.toDouble(),
topK = genSettings.topK.coerceAtLeast(1)
Expand All @@ -173,7 +173,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
// or we use a fallback model for non-live operations
// Using the first non-live model as fallback
val fallbackModel = GenerativeModel(
modelName = ModelOption.GEMINI_FLASH_PREVIEW.modelName, // Using Gemini 2.5 Flash as fallback
modelName = com.google.ai.sample.util.ModelIdentifierOverrides.resolve(ModelOption.GEMINI_FLASH_PREVIEW), // Using Gemini 2.5 Flash as fallback
apiKey = apiKey,
generationConfig = config
)
Expand All @@ -190,7 +190,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
} else {
// Regular generative models
val generativeModel = GenerativeModel(
modelName = currentModel.modelName,
modelName = com.google.ai.sample.util.ModelIdentifierOverrides.resolve(currentModel),
apiKey = apiKey,
generationConfig = config
)
Expand Down
36 changes: 23 additions & 13 deletions app/src/main/kotlin/com/google/ai/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class MainActivity : ComponentActivity() {

private fun handleMediaProjectionPermissionDenied() {
Log.w(TAG, "MediaProjection permission denied or cancelled by user.")
Toast.makeText(this, "Screen capture permission denied", Toast.LENGTH_SHORT).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_screen_capture_permission_denied", "Screen capture permission denied"), Toast.LENGTH_SHORT).show()
resetExplicitScreenshotRequestFlagIfNeeded("explicit denial")
_isMediaProjectionPermissionGranted.value = false
}
Expand All @@ -332,7 +332,7 @@ class MainActivity : ComponentActivity() {

private fun handleWebRtcMediaProjectionPermissionDenied() {
Log.w(TAG, "WebRTC MediaProjection permission denied.")
Toast.makeText(this, "Screen capture permission denied", Toast.LENGTH_SHORT).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_screen_capture_permission_denied", "Screen capture permission denied"), Toast.LENGTH_SHORT).show()
onWebRtcMediaProjectionResult = null
}

Expand All @@ -354,18 +354,18 @@ class MainActivity : ComponentActivity() {
private fun registerPermissionLaunchers() {
requestNotificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
Toast.makeText(this, "Notification permission granted.", Toast.LENGTH_SHORT).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_notification_permission_granted", "Notification permission granted."), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Notification permission denied. Stop via notification will not be available.", Toast.LENGTH_LONG).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_notification_permission_denied", "Notification permission denied. Stop via notification will not be available."), Toast.LENGTH_LONG).show()
}
}

requestForegroundServicePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
Toast.makeText(this, "Foreground service permission granted.", Toast.LENGTH_SHORT).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_foreground_service_permission_granted", "Foreground service permission granted."), Toast.LENGTH_SHORT).show()
launchCaptureIntent(mediaProjectionLauncher, "requestForegroundServicePermissionLauncher")
} else {
Toast.makeText(this, "Foreground service permission denied. The app may not function correctly.", Toast.LENGTH_LONG).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_foreground_service_permission_denied", "Foreground service permission denied. The app may not function correctly."), Toast.LENGTH_LONG).show()
}
}
}
Expand Down Expand Up @@ -465,7 +465,7 @@ class MainActivity : ComponentActivity() {
// This situation (service not running but takeAdditionalScreenshot called directly)
// should ideally be handled by the caller checking isRunning() first.
// If called from screenshotRequestHandler, it would have called requestMediaProjectionPermission instead.
Toast.makeText(this, "Screenshot service not active. Please grant permission first.", Toast.LENGTH_LONG).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_screenshot_service_not_active", "Screenshot service not active. Please grant permission first."), Toast.LENGTH_LONG).show()
// Optionally, trigger permission request again if appropriate for the use case.
// requestMediaProjectionPermission() // This might be too aggressive if called from unexpected places.
}
Expand Down Expand Up @@ -560,10 +560,10 @@ class MainActivity : ComponentActivity() {
}
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
Log.i(TAG, "purchasesUpdatedListener: User cancelled the purchase flow.")
Toast.makeText(this, "Support cancelled.", Toast.LENGTH_SHORT).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_support_cancelled", "Support cancelled."), Toast.LENGTH_SHORT).show()
} else {
Log.e(TAG, "purchasesUpdatedListener: Billing error: ${billingResult.debugMessage} (Code: ${billingResult.responseCode})")
Toast.makeText(this, "Error during donation process: ${billingResult.debugMessage}", Toast.LENGTH_LONG).show()
Toast.makeText(this, com.google.ai.sample.util.UiStringsConfig.get("toast_donation_error", "Error during donation process: {0}", billingResult.debugMessage), Toast.LENGTH_LONG).show()
}
}

Expand Down Expand Up @@ -744,7 +744,7 @@ class MainActivity : ComponentActivity() {
onDismiss = { showPaymentMethodDialog = false },
onPayPalClick = {
showPaymentMethodDialog = false
Toast.makeText(this@MainActivity, "PayPal ist in dieser Fallback-UI noch nicht verfügbar.", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, com.google.ai.sample.util.UiStringsConfig.get("toast_paypal_fallback_unavailable", "PayPal ist in dieser Fallback-UI noch nicht verfügbar."), Toast.LENGTH_LONG).show()
},
onGooglePlayClick = {
showPaymentMethodDialog = false
Expand Down Expand Up @@ -1202,15 +1202,15 @@ class MainActivity : ComponentActivity() {
updateTrialState(TrialManager.getTrialState(this, null))
if (currentTrialState == TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED) {
Log.i(TAG, "queryActiveSubscriptions: Subscription deactivated (no active sub and trial expired). Showing Toast.")
Toast.makeText(this@MainActivity, "Subscription is deactivated", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, com.google.ai.sample.util.UiStringsConfig.get("toast_subscription_deactivated", "Subscription is deactivated"), Toast.LENGTH_LONG).show()
}
startTrialServiceIfNeeded()
} else {
Log.w(TAG, "queryActiveSubscriptions: No active sub from Google, but TrialManager says PURCHASED. This could be due to restored SharedPreferences without active subscription. Re-evaluating trial logic based on no internet time.")
updateTrialState(TrialManager.getTrialState(this, null))
if (currentTrialState == TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED) {
Log.i(TAG, "queryActiveSubscriptions: Subscription deactivated (no active sub, was purchased, now trial expired). Showing Toast.")
Toast.makeText(this@MainActivity, "Subscription is deactivated", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, com.google.ai.sample.util.UiStringsConfig.get("toast_subscription_deactivated", "Subscription is deactivated"), Toast.LENGTH_LONG).show()
}
startTrialServiceIfNeeded()
}
Expand All @@ -1226,7 +1226,7 @@ class MainActivity : ComponentActivity() {
updateTrialState(TrialManager.getTrialState(this, null))
if (currentTrialState == TrialManager.TrialState.EXPIRED_INTERNET_TIME_CONFIRMED) {
Log.i(TAG, "queryActiveSubscriptions: Subscription deactivated (query failed, trial expired). Showing Toast.")
Toast.makeText(this@MainActivity, "Subscription is deactivated", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, com.google.ai.sample.util.UiStringsConfig.get("toast_subscription_deactivated", "Subscription is deactivated"), Toast.LENGTH_LONG).show()
}
startTrialServiceIfNeeded()
}
Expand Down Expand Up @@ -1432,6 +1432,16 @@ class MainActivity : ComponentActivity() {
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
}

/**
* Evaluates a JavaScript expression in the active WebView on the UI thread.
* No-op if no WebView is currently loaded.
*/
fun evaluateWebViewJs(js: String) {
webViewInstance?.post {
webViewInstance?.evaluateJavascript(js, null)
}
}

/**
* Escapes a string so it can be safely embedded inside a single-quoted JS string literal
* passed to [WebView.evaluateJavascript]. Delegates to [WebViewBridge.jsEscape] so both
Expand Down
28 changes: 16 additions & 12 deletions app/src/main/kotlin/com/google/ai/sample/MainActivityDialogs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ internal fun PaymentMethodDialog(
onPayPalClick: () -> Unit,
onGooglePlayClick: () -> Unit
) {
val ui = com.google.ai.sample.util.TrialUiConfig.current()
androidx.compose.material3.AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Choose Payment Method") },
title = { Text(ui.paymentMethodDialogTitle) },
text = {
Column {
Button(
Expand All @@ -69,20 +70,20 @@ internal fun PaymentMethodDialog(
containerColor = Color.Gray
)
) {
Text("PayPal (2,90 €/Month)")
Text(ui.paymentMethodPayPalButtonLabel)
}
Button(
onClick = onGooglePlayClick,
modifier = Modifier.fillMaxWidth()
) {
Text("Google Play (2,90 €/Month)")
Text(ui.paymentMethodGooglePlayButtonLabel)
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
Text(ui.paymentMethodCancelButtonLabel)
}
}
)
Expand All @@ -106,6 +107,7 @@ internal fun ApiKeyDialogSection(
@Composable
fun FirstLaunchInfoDialog(onDismiss: () -> Unit) {
Log.d("FirstLaunchInfoDialog", "Composing FirstLaunchInfoDialog")
val ui = com.google.ai.sample.util.TrialUiConfig.current()
Dialog(onDismissRequest = {
Log.d("FirstLaunchInfoDialog", "onDismissRequest called")
onDismiss()
Expand All @@ -123,12 +125,12 @@ fun FirstLaunchInfoDialog(onDismiss: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Trial Information",
text = ui.firstLaunchDialogTitle,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "You can try Screen Operator for 7 days before you have to subscribe to support the development of more features.",
text = ui.firstLaunchDialogBody,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Expand All @@ -140,7 +142,7 @@ fun FirstLaunchInfoDialog(onDismiss: () -> Unit) {
},
modifier = Modifier.fillMaxWidth()
) {
Text("OK")
Text(ui.firstLaunchDialogButton)
}
}
}
Expand All @@ -155,6 +157,7 @@ fun TrialExpiredDialog(
@Suppress("UNUSED_PARAMETER") onDismiss: () -> Unit
) {
Log.d("TrialExpiredDialog", "Composing TrialExpiredDialog")
val ui = com.google.ai.sample.util.TrialUiConfig.current()
Dialog(onDismissRequest = {
Log.d("TrialExpiredDialog", "onDismissRequest called (persistent dialog - user tried to dismiss)")
}) {
Expand All @@ -171,12 +174,12 @@ fun TrialExpiredDialog(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Trial period expired",
text = ui.trialExpiredDialogTitle,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Please support the development of the app so that you can continue using it \uD83C\uDF89",
text = ui.trialExpiredDialogBody,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Expand All @@ -188,7 +191,7 @@ fun TrialExpiredDialog(
},
modifier = Modifier.fillMaxWidth()
) {
Text("Subscribe")
Text(ui.trialExpiredDialogSubscribeButton)
}
}
}
Expand All @@ -201,6 +204,7 @@ fun InfoDialog(
onDismiss: () -> Unit
) {
Log.d("InfoDialog", "Composing InfoDialog with message: $message")
val ui = com.google.ai.sample.util.TrialUiConfig.current()
Dialog(onDismissRequest = {
Log.d("InfoDialog", "onDismissRequest called")
onDismiss()
Expand All @@ -218,7 +222,7 @@ fun InfoDialog(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Information",
text = ui.infoDialogTitle,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(16.dp))
Expand All @@ -232,7 +236,7 @@ fun InfoDialog(
Log.d("InfoDialog", "OK button clicked")
onDismiss()
}) {
Text("OK")
Text(com.google.ai.sample.util.UiStringsConfig.get("apikey_dialog_ok", "OK"))
}
}
}
Expand Down
Loading
Loading