Skip to content
Open
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
Binary file removed app-release-signed.apk
Binary file not shown.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest")

implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
implementation("com.google.mediapipe:tasks-genai:0.10.27")
implementation("com.google.mediapipe:tasks-vision:0.10.14")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
}
Expand Down
16 changes: 6 additions & 10 deletions app/src/main/kotlin/com/google/ai/sample/ApiKeyDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import androidx.compose.ui.window.Dialog
fun ApiKeyDialog(
apiKeyManager: ApiKeyManager,
isFirstLaunch: Boolean = false,
initialProvider: ApiProvider? = null,
onDismiss: () -> Unit
) {
var apiKeyInput by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf("") }
var selectedProvider by remember { mutableStateOf(ApiProvider.VERCEL) }
var selectedProvider by remember { mutableStateOf(initialProvider ?: ApiProvider.VERCEL) }
val apiKeys = remember { mutableStateMapOf<ApiProvider, List<String>>() }
var selectedKeyIndex by remember { mutableStateOf(apiKeyManager.getCurrentKeyIndex(selectedProvider)) }
val context = LocalContext.current
Expand All @@ -43,11 +44,7 @@ fun ApiKeyDialog(
loadKeysForProvider(ApiProvider.CEREBRAS)
}

Dialog(onDismissRequest = {
if (!isFirstLaunch || (apiKeys[ApiProvider.GOOGLE]?.isNotEmpty() == true || apiKeys[ApiProvider.CEREBRAS]?.isNotEmpty() == true)) {
onDismiss()
}
}) {
Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -89,6 +86,7 @@ fun ApiKeyDialog(
ApiProvider.GOOGLE -> "https://makersuite.google.com/app/apikey"
ApiProvider.CEREBRAS -> "https://cloud.cerebras.ai/"
ApiProvider.VERCEL -> "https://vercel.com/ai-gateway"
ApiProvider.OFFLINE_GEMMA -> "https://ai.google.dev/edge/gallery"
}
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)
Expand Down Expand Up @@ -194,10 +192,8 @@ fun ApiKeyDialog(
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
if (!isFirstLaunch || (apiKeys[ApiProvider.VERCEL]?.isNotEmpty() == true || apiKeys[ApiProvider.GOOGLE]?.isNotEmpty() == true || apiKeys[ApiProvider.CEREBRAS]?.isNotEmpty() == true)) {
TextButton(onClick = onDismiss) {
Text("Close")
}
TextButton(onClick = onDismiss) {
Text("Close")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import com.google.ai.sample.feature.multimodal.PhotoReasoningViewModel
enum class ApiProvider {
VERCEL,
GOOGLE,
CEREBRAS
CEREBRAS,
OFFLINE_GEMMA
}

enum class ModelOption(val displayName: String, val modelName: String, val apiProvider: ApiProvider = ApiProvider.GOOGLE) {
Expand All @@ -30,7 +31,8 @@ enum class ModelOption(val displayName: String, val modelName: String, val apiPr
GEMINI_FLASH("Gemini 2.0 Flash", "gemini-2.0-flash"),
GEMINI_FLASH_LITE("Gemini 2.0 Flash Lite", "gemini-2.0-flash-lite"),
GEMMA_3_27B_IT("Gemma 3 27B IT", "gemma-3-27b-it"),
GEMMA_3N_E4B_IT("Gemma 3n E4B it (online)", "gemma-3n-e4b-it")
GEMMA_3N_E4B_IT("Gemma 3n E4B it (online)", "gemma-3n-e4b-it"),
GEMMA_3N_E4B_IT_OFFLINE("Gemma 3n E4B it (offline GPU)", "gemma-3n-e4b-it-offline", ApiProvider.OFFLINE_GEMMA)
}

val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
Expand All @@ -48,9 +50,14 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
// Get the API key from MainActivity
val mainActivity = MainActivity.getInstance()
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
val apiKey = mainActivity?.getCurrentApiKey(currentModel.apiProvider) ?: ""

if (apiKey.isEmpty()) {
val apiKey = if (currentModel.apiProvider == ApiProvider.OFFLINE_GEMMA) {
"OFFLINE"
} else {
mainActivity?.getCurrentApiKey(currentModel.apiProvider) ?: ""
}

if (apiKey.isEmpty() && currentModel.apiProvider != ApiProvider.OFFLINE_GEMMA) {
throw IllegalStateException("API key for ${currentModel.apiProvider} is not available. Please set an API key.")
}

Expand All @@ -59,7 +66,21 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
isAssignableFrom(PhotoReasoningViewModel::class.java) -> {
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()

if (currentModel.modelName.contains("live")) {
if (currentModel.apiProvider == ApiProvider.OFFLINE_GEMMA) {
// Offline Gemma model
// We use a dummy GenerativeModel because PhotoReasoningViewModel expects one
// but it will use MediaPipe for actual inference.
val dummyModel = GenerativeModel(
modelName = currentModel.modelName,
apiKey = "OFFLINE",
generationConfig = config
)
PhotoReasoningViewModel(
dummyModel,
currentModel.modelName,
null
)
} else if (currentModel.modelName.contains("live")) {
// Live API models
val liveApiManager = LiveApiManager(apiKey, currentModel.modelName)

Expand Down
14 changes: 6 additions & 8 deletions app/src/main/kotlin/com/google/ai/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class MainActivity : ComponentActivity() {
private var photoReasoningViewModel: PhotoReasoningViewModel? = null
private lateinit var apiKeyManager: ApiKeyManager
private var showApiKeyDialog by mutableStateOf(false)
private var selectedProviderForDialog by mutableStateOf<ApiProvider?>(null)

// Google Play Billing
private lateinit var billingClient: BillingClient
Expand Down Expand Up @@ -381,12 +382,6 @@ class MainActivity : ComponentActivity() {

apiKeyManager = ApiKeyManager.getInstance(this)
Log.d(TAG, "onCreate: ApiKeyManager initialized.")
if (apiKeyManager.getApiKeys(ApiProvider.GOOGLE).isEmpty() && apiKeyManager.getApiKeys(ApiProvider.CEREBRAS).isEmpty()) {
showApiKeyDialog = true
Log.d(TAG, "onCreate: No API key found, showApiKeyDialog set to true.")
} else {
Log.d(TAG, "onCreate: API key found.")
}

// Log.d(TAG, "onCreate: Calling checkAndRequestPermissions.") // Deleted
// checkAndRequestPermissions() // Deleted
Expand Down Expand Up @@ -548,9 +543,11 @@ class MainActivity : ComponentActivity() {
ApiKeyDialog(
apiKeyManager = apiKeyManager,
isFirstLaunch = apiKeyManager.getApiKeys(ApiProvider.GOOGLE).isEmpty() && apiKeyManager.getApiKeys(ApiProvider.CEREBRAS).isEmpty(),
initialProvider = selectedProviderForDialog,
onDismiss = {
Log.d(TAG, "ApiKeyDialog onDismiss called.")
showApiKeyDialog = false
selectedProviderForDialog = null
}
)
} else {
Expand Down Expand Up @@ -736,8 +733,9 @@ class MainActivity : ComponentActivity() {
Log.w(TAG, "MenuScreen: Navigation to '$routeId' blocked due to trial state.")
}
},
onApiKeyButtonClicked = {
Log.d(TAG, "MenuScreen onApiKeyButtonClicked: Showing ApiKeyDialog.")
onApiKeyButtonClicked = { provider ->
Log.d(TAG, "MenuScreen onApiKeyButtonClicked: Showing ApiKeyDialog for $provider.")
selectedProviderForDialog = provider
showApiKeyDialog = true
},
onDonationButtonClicked = {
Expand Down
50 changes: 48 additions & 2 deletions app/src/main/kotlin/com/google/ai/sample/MenuScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import android.Manifest // For Manifest.permission.POST_NOTIFICATIONS
import androidx.compose.material3.AlertDialog // For the rationale dialog
import androidx.compose.runtime.saveable.rememberSaveable
import android.util.Log
import com.google.ai.sample.util.ModelDownloadManager

data class MenuItem(
val routeId: String,
Expand All @@ -52,7 +53,7 @@ data class MenuItem(
fun MenuScreen(
innerPadding: PaddingValues,
onItemClicked: (String) -> Unit = { },
onApiKeyButtonClicked: () -> Unit = { },
onApiKeyButtonClicked: (ApiProvider?) -> Unit = { },
onDonationButtonClicked: () -> Unit = { },
isTrialExpired: Boolean = false, // New parameter to indicate trial status
isPurchased: Boolean = false
Expand All @@ -67,6 +68,7 @@ fun MenuScreen(
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
var selectedModel by remember { mutableStateOf(currentModel) }
var expanded by remember { mutableStateOf(false) }
var showDownloadDialog by remember { mutableStateOf(false) }

Column(
modifier = Modifier
Expand Down Expand Up @@ -95,7 +97,7 @@ fun MenuScreen(
modifier = Modifier.weight(1f)
)
Button(
onClick = { onApiKeyButtonClicked() },
onClick = { onApiKeyButtonClicked(null) },
enabled = true, // Always enabled
modifier = Modifier.padding(start = 8.dp)
) {
Expand Down Expand Up @@ -157,6 +159,12 @@ fun MenuScreen(
selectedModel = modelOption
GenerativeAiViewModelFactory.setModel(modelOption)
expanded = false

if (modelOption.apiProvider == ApiProvider.OFFLINE_GEMMA) {
if (!ModelDownloadManager.isModelDownloaded(context)) {
showDownloadDialog = true
}
}
},
enabled = true // Always enabled
)
Expand Down Expand Up @@ -193,6 +201,21 @@ fun MenuScreen(
if (isTrialExpired) {
Toast.makeText(context, "Please subscribe to the app to continue.", Toast.LENGTH_LONG).show()
} else {
val currentModelOption = GenerativeAiViewModelFactory.getCurrentModel()
if (currentModelOption.apiProvider == ApiProvider.OFFLINE_GEMMA) {
if (!ModelDownloadManager.isModelDownloaded(context)) {
showDownloadDialog = true
return@TextButton
}
} else {
val mainActivity = context as? MainActivity
val apiKey = mainActivity?.getCurrentApiKey(currentModelOption.apiProvider)
if (apiKey.isNullOrEmpty()) {
onApiKeyButtonClicked(currentModelOption.apiProvider)
return@TextButton
}
}

if (menuItem.routeId == "photo_reasoning") {
val mainActivity = context as? MainActivity
if (mainActivity != null) { // Ensure mainActivity is not null
Expand Down Expand Up @@ -314,6 +337,29 @@ GPT-5 nano Input: $0.05/M Output: $0.40/M
}
}

if (showDownloadDialog) {
AlertDialog(
onDismissRequest = { showDownloadDialog = false },
title = { Text("Download Model") },
text = {
val availableSpace = "%.2f".format(ModelDownloadManager.getAvailableStorageGB(context))
Text("Should Gemma 3n E4B be downloaded? It requires 4.7 GB.\n\nAvailable storage: $availableSpace GB")
},
confirmButton = {
TextButton(
onClick = {
ModelDownloadManager.downloadModel(context)
showDownloadDialog = false
Toast.makeText(context, "Download started...", Toast.LENGTH_SHORT).show()
}
) { Text("OK") }
Comment on lines +348 to +355

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: The download dialog doesn't check if there's sufficient storage space before allowing the download. Users could attempt to download a 4.7 GB model without enough space, leading to failed downloads and poor user experience.

Suggested change
confirmButton = {
TextButton(
onClick = {
ModelDownloadManager.downloadModel(context)
showDownloadDialog = false
Toast.makeText(context, "Download started...", Toast.LENGTH_SHORT).show()
}
) { Text("OK") }
confirmButton = {
TextButton(
onClick = {
val availableSpace = ModelDownloadManager.getAvailableStorageGB(context)
if (availableSpace < 5.0) { // Need at least 5GB for 4.7GB model + buffer
Toast.makeText(context, "Insufficient storage space. Need at least 5 GB available.", Toast.LENGTH_LONG).show()
} else {
ModelDownloadManager.downloadModel(context)
showDownloadDialog = false
Toast.makeText(context, "Download started...", Toast.LENGTH_SHORT).show()
}
}
) { Text("OK") }

},
dismissButton = {
TextButton(onClick = { showDownloadDialog = false }) { Text("Abort") }
}
)
}

if (showRationaleDialogForPhotoReasoning) {
val mainActivity = LocalContext.current as? MainActivity
AlertDialog(
Expand Down
Loading