Skip to content
Merged
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 @@ -59,7 +59,7 @@ enum class ModelOption(

val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
viewModelClass: Class<T>,
modelClass: Class<T>,
extras: CreationExtras
): T {
// Get the application context from extras
Expand All @@ -86,7 +86,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
throw IllegalStateException("API key for ${currentModel.apiProvider} is not available. Please set an API key.")
}

return with(viewModelClass) {
val createdViewModel = with(modelClass) {
when {
isAssignableFrom(PhotoReasoningViewModel::class.java) -> {
if (currentModel.modelName.contains("live")) {
Expand Down Expand Up @@ -128,9 +128,11 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
}

else ->
throw IllegalArgumentException("Unknown ViewModel class: ${viewModelClass.name}")
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}

return modelClass.cast(createdViewModel)
}
}

Expand Down
9 changes: 7 additions & 2 deletions app/src/main/kotlin/com/google/ai/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.PendingPurchasesParams
import com.google.ai.sample.feature.multimodal.PhotoReasoningRoute
import com.google.ai.sample.feature.multimodal.PhotoReasoningViewModel
import com.google.ai.sample.GenerativeAiViewModelFactory
Expand Down Expand Up @@ -840,7 +841,11 @@ class MainActivity : ComponentActivity() {

billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build()
)
.build()
Log.d(TAG, "setupBillingClient: BillingClient built. Starting connection.")

Expand Down Expand Up @@ -1308,7 +1313,7 @@ fun FirstLaunchInfoDialog(onDismiss: () -> Unit) {
@Composable
fun TrialExpiredDialog(
onPurchaseClick: () -> Unit,
onDismiss: () -> Unit
@Suppress("UNUSED_PARAMETER") onDismiss: () -> Unit
) {
Log.d("TrialExpiredDialog", "Composing TrialExpiredDialog")
Dialog(onDismissRequest = {
Expand Down
11 changes: 5 additions & 6 deletions app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,14 @@ fun MenuScreen(
} else {
if (menuItem.routeId == "photo_reasoning") {
val mainActivity = context as? MainActivity
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
val activeModel = GenerativeAiViewModelFactory.getCurrentModel()
// Check API Key for online models
if (currentModel.apiProvider != ApiProvider.GOOGLE || !currentModel.modelName.contains("litert")) { // Simple check, refine if needed. Actually offline model has specific Enum
if (currentModel != ModelOption.GEMMA_3N_E4B_IT && currentModel != ModelOption.HUMAN_EXPERT) {
val apiKey = mainActivity?.getCurrentApiKey(currentModel.apiProvider)
if (activeModel.apiProvider != ApiProvider.GOOGLE || !activeModel.modelName.contains("litert")) { // Simple check, refine if needed. Actually offline model has specific Enum
if (activeModel != ModelOption.GEMMA_3N_E4B_IT && activeModel != ModelOption.HUMAN_EXPERT) {
val apiKey = mainActivity?.getCurrentApiKey(activeModel.apiProvider)
if (apiKey.isNullOrEmpty()) {
// Show API Key Dialog
onApiKeyButtonClicked(currentModel.apiProvider) // Or a specific callback to show dialog
onApiKeyButtonClicked(activeModel.apiProvider) // Or a specific callback to show dialog
return@TextButton
}
}
Expand Down Expand Up @@ -675,7 +675,6 @@ fun MenuScreen(
}

if (showDownloadDialog && downloadDialogModel != null) {
val context = LocalContext.current
val statFs = StatFs(Environment.getExternalStorageDirectory().path)
val bytesAvailable = statFs.availableBlocksLong * statFs.blockSizeLong
val gbAvailable = bytesAvailable.toDouble() / (1024 * 1024 * 1024)
Expand Down
19 changes: 8 additions & 11 deletions app/src/main/kotlin/com/google/ai/sample/ScreenCaptureService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.MissingFieldException
Expand All @@ -56,8 +57,8 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.io.IOException
import java.io.File
import java.io.IOException
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
Expand Down Expand Up @@ -569,15 +570,10 @@ class ScreenCaptureService : Service() {
val displayMetrics = DisplayMetrics()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val defaultDisplay = windowManager.defaultDisplay
if (defaultDisplay != null) {
defaultDisplay.getRealMetrics(displayMetrics)
} else {
val bounds = windowManager.currentWindowMetrics.bounds
displayMetrics.widthPixels = bounds.width()
displayMetrics.heightPixels = bounds.height()
displayMetrics.densityDpi = resources.displayMetrics.densityDpi
}
val bounds = windowManager.currentWindowMetrics.bounds
displayMetrics.widthPixels = bounds.width()
displayMetrics.heightPixels = bounds.height()
displayMetrics.densityDpi = resources.displayMetrics.densityDpi
} else {
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(displayMetrics)
Expand Down Expand Up @@ -876,6 +872,7 @@ data class ServiceMistralMessage(
)

@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("type")
sealed class ServiceMistralContent

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
info.notificationTimeout = 100
info.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or
AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or
AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY or
AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS

// Apply the configuration
Expand Down Expand Up @@ -567,31 +566,29 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {

override fun onAccessibilityEvent(event: AccessibilityEvent) {
// Process accessibility events
event?.let {
when (it.eventType) {
AccessibilityEvent.TYPE_VIEW_CLICKED -> {
Log.d(TAG, "Accessibility event: View clicked")
}
AccessibilityEvent.TYPE_VIEW_FOCUSED -> {
Log.d(TAG, "Accessibility event: View focused")
}
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
Log.d(TAG, "Accessibility event: Window state changed")
}
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
Log.d(TAG, "Accessibility event: Window content changed")
}
else -> {
// Handle all other event types
Log.d(TAG, "Accessibility event: Other event type: ${it.eventType}")
}
when (event.eventType) {
AccessibilityEvent.TYPE_VIEW_CLICKED -> {
Log.d(TAG, "Accessibility event: View clicked")
}

// Refresh the root node when window state or content changes
if (it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
it.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
refreshRootNode()
AccessibilityEvent.TYPE_VIEW_FOCUSED -> {
Log.d(TAG, "Accessibility event: View focused")
}
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
Log.d(TAG, "Accessibility event: Window state changed")
}
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
Log.d(TAG, "Accessibility event: Window content changed")
}
else -> {
// Handle all other event types
Log.d(TAG, "Accessibility event: Other event type: ${event.eventType}")
}
}

// Refresh the root node when window state or content changes
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
refreshRootNode()
}
}

Expand Down Expand Up @@ -875,7 +872,7 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
val centerX = rect.centerX()
val centerY = rect.centerY()

Log.d(TAG, "Trying to tap at the center of the button: ($centerX, $centerY)")
Log.d(TAG, "Trying alternative tap for button \"$buttonText\" at center: ($centerX, $centerY)")
showToast("Trying to tap coordinates: ($centerX, $centerY)", false)

// Tap at the center of the button
Expand Down Expand Up @@ -1709,7 +1706,9 @@ private fun openAppUsingLaunchIntent(packageName: String, appName: String): Bool
.replace("_", " ")
.replace(Regex("([a-z])([A-Z])"), "$1 $2")
.lowercase(Locale.getDefault())
.capitalize(Locale.getDefault())
.replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else char.toString()
}

// If it contains common button names like "new", "add", etc., return it
val commonButtonNames = listOf("new", "add", "edit", "delete", "save", "cancel", "ok", "send")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.rounded.Add
import androidx.compose.foundation.BorderStroke
Expand All @@ -52,13 +52,13 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
Expand Down Expand Up @@ -159,7 +159,6 @@ internal fun PhotoReasoningRoute(
val commandExecutionStatus by viewModel.commandExecutionStatus.collectAsState()
val detectedCommands by viewModel.detectedCommands.collectAsState()
val systemMessage by viewModel.systemMessage.collectAsState()
val chatMessages by viewModel.chatMessagesFlow.collectAsState()
val isInitialized by viewModel.isInitialized.collectAsState()
val modelName by viewModel.modelNameState.collectAsState()
val userInput by viewModel.userInput.collectAsState()
Expand Down Expand Up @@ -226,7 +225,6 @@ internal fun PhotoReasoningRoute(
isAccessibilityServiceEnabled = isAccessibilityServiceEffectivelyEnabled,
isMediaProjectionPermissionGranted = isMediaProjectionPermissionGranted,
onEnableAccessibilityService = {
val intent = Settings.ACTION_ACCESSIBILITY_SETTINGS
try {
// val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) // Corrected
// accessibilitySettingsLauncher.launch(intent) // Corrected below
Expand Down Expand Up @@ -481,7 +479,12 @@ fun PhotoReasoningScreen(
else -> command::class.simpleName ?: "Unknown Command"
}
Text("${index + 1}. $commandText", color = MaterialTheme.colorScheme.onTertiaryContainer)
if (index < detectedCommands.size - 1) Divider(Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.2f))
if (index < detectedCommands.size - 1) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.2f)
)
}
}
}
}
Expand Down Expand Up @@ -555,7 +558,7 @@ fun PhotoReasoningScreen(
modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)
) {
Icon(
Icons.Default.Send,
Icons.AutoMirrored.Filled.Send,
stringResource(R.string.action_go),
tint = if (isInitialized && userQuestion.isNotBlank())
MaterialTheme.colorScheme.primary else Color.Gray,
Expand Down Expand Up @@ -784,14 +787,19 @@ fun DatabaseListPopup(
)
}
} ?: Log.w(TAG_IMPORT_PROCESS, "ContentResolver.openInputStream returned null for URI: $uri (second check).")
} catch (oom: OutOfMemoryError) {
Log.e(TAG_IMPORT_PROCESS, "Out of memory during file import for URI: $uri on thread: ${Thread.currentThread().name}", oom)
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"Error importing file: Out of memory. File may be too large or contain too many entries." as CharSequence,
Toast.LENGTH_LONG
).show()
}
} catch (e: Exception) {
Log.e(TAG_IMPORT_PROCESS, "Error during file import for URI: $uri on thread: ${Thread.currentThread().name}", e)
withContext(Dispatchers.Main) {
val errorMessage = if (e is OutOfMemoryError) {
"Out of memory. File may be too large or contain too many entries."
} else {
e.message ?: "Unknown error during import."
}
val errorMessage = e.message ?: "Unknown error during import."
Toast.makeText(context, "Error importing file: $errorMessage" as CharSequence, Toast.LENGTH_LONG).show()
}
}
Expand Down Expand Up @@ -1016,7 +1024,7 @@ fun OverwriteConfirmationDialog(
entryTitle: String,
onConfirm: () -> Unit,
onDeny: () -> Unit,
onSkipAll: () -> Unit,
onSkipAll: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
Expand All @@ -1027,7 +1035,10 @@ fun OverwriteConfirmationDialog(
TextButton(onClick = onConfirm) { Text("Yes") }
},
dismissButton = {
TextButton(onClick = onDeny) { Text("No") }
Row {
TextButton(onClick = onSkipAll) { Text("Skip All") }
TextButton(onClick = onDeny) { Text("No") }
}
}
)
}
Expand Down
Loading