From 3e122f2e82f96703ed720e743c7ff2f2f5d69a2b Mon Sep 17 00:00:00 2001 From: Rosario Fernandes Date: Tue, 16 Dec 2025 22:42:47 +0000 Subject: [PATCH 1/5] feat: create SVG generator --- admob/app/build.gradle.kts | 6 +- admob/build.gradle.kts | 6 +- analytics/app/build.gradle.kts | 4 +- analytics/build.gradle.kts | 6 +- appdistribution/app/build.gradle.kts | 2 +- appdistribution/build.gradle.kts | 6 +- auth/app/build.gradle.kts | 10 +- auth/build.gradle.kts | 6 +- build.gradle.kts | 12 +- config/app/build.gradle.kts | 2 +- config/build.gradle.kts | 6 +- crash/app/build.gradle.kts | 4 +- crash/build.gradle.kts | 6 +- database/app/build.gradle.kts | 8 +- database/build.gradle.kts | 6 +- firebase-ai/app/build.gradle.kts | 12 ++ firebase-ai/app/src/main/AndroidManifest.xml | 7 +- .../quickstart/ai/FirebaseAISamples.kt | 85 ++++++++++- .../firebase/quickstart/ai/MainActivity.kt | 37 ++++- .../ai/feature/live/BidiViewModel.kt | 84 +++++------ .../quickstart/ai/feature/live/CameraView.kt | 98 ++++++++++++ .../ai/feature/live/StreamRealtimeScreen.kt | 2 - .../feature/live/StreamRealtimeVideoScreen.kt | 82 ++++++++++ .../ai/feature/media/imagen/EditingMode.kt | 1 + .../feature/media/imagen/ImagenViewModel.kt | 31 +++- .../quickstart/ai/feature/svg/SvgScreen.kt | 131 ++++++++++++++++ .../quickstart/ai/feature/svg/SvgViewModel.kt | 72 +++++++++ .../quickstart/ai/feature/text/ChatScreen.kt | 3 +- .../ai/feature/text/ChatViewModel.kt | 5 +- .../ai/feature/text/TextGenScreen.kt | 142 ++++++++++++++++++ .../ai/feature/text/TextGenViewModel.kt | 83 ++++++++++ .../quickstart/ai/ui/navigation/Sample.kt | 2 + .../app/src/main/res/values/colors.xml | 7 - firebase-ai/build.gradle.kts | 10 +- firebase-ai/gradle/libs.versions.toml | 22 ++- firebase-ai/settings.gradle.kts | 1 + firestore/app/build.gradle.kts | 14 +- firestore/build.gradle.kts | 8 +- functions/app/build.gradle.kts | 6 +- functions/build.gradle.kts | 6 +- functions/functions/package-lock.json | 41 ++--- gradle/libs.versions.toml | 54 ++++--- inappmessaging/app/build.gradle.kts | 2 +- inappmessaging/build.gradle.kts | 6 +- internal/lint/build.gradle.kts | 6 +- messaging/app/build.gradle.kts | 4 +- messaging/build.gradle.kts | 6 +- perf/app/build.gradle.kts | 4 +- perf/build.gradle.kts | 8 +- storage/app/build.gradle.kts | 4 +- storage/build.gradle.kts | 6 +- 51 files changed, 972 insertions(+), 210 deletions(-) create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgScreen.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenScreen.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenViewModel.kt diff --git a/admob/app/build.gradle.kts b/admob/app/build.gradle.kts index 6bf9146de9..de961b22ed 100644 --- a/admob/app/build.gradle.kts +++ b/admob/app/build.gradle.kts @@ -55,13 +55,13 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("com.google.android.material:material:1.13.0") implementation("androidx.browser:browser:1.5.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.9.5") - implementation("androidx.navigation:navigation-ui-ktx:2.9.5") + implementation("androidx.navigation:navigation-fragment-ktx:2.9.6") + implementation("androidx.navigation:navigation-ui-ktx:2.9.6") implementation("com.google.android.gms:play-services-ads:23.3.0") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // For an optimal experience using AdMob, add the Firebase SDK // for Google Analytics. This is recommended, but not required. diff --git a/admob/build.gradle.kts b/admob/build.gradle.kts index c07891342a..06b429a6b4 100644 --- a/admob/build.gradle.kts +++ b/admob/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/analytics/app/build.gradle.kts b/analytics/app/build.gradle.kts index 160159b63a..f2e72793c5 100644 --- a/analytics/app/build.gradle.kts +++ b/analytics/app/build.gradle.kts @@ -56,10 +56,10 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.preference:preference-ktx:1.2.1") // Needed to override the version used by preference-ktx - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Analytics implementation("com.google.firebase:firebase-analytics") diff --git a/analytics/build.gradle.kts b/analytics/build.gradle.kts index 5e59ee2b4f..cf3c2c9ca4 100644 --- a/analytics/build.gradle.kts +++ b/analytics/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/appdistribution/app/build.gradle.kts b/appdistribution/app/build.gradle.kts index 943a1d5083..42e795fbb0 100644 --- a/appdistribution/app/build.gradle.kts +++ b/appdistribution/app/build.gradle.kts @@ -57,7 +57,7 @@ dependencies { implementation("androidx.multidex:multidex:2.0.1") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // ADD the SDK to the "prerelease" variant only (example) implementation("com.google.firebase:firebase-appdistribution:16.0.0-beta17") diff --git a/appdistribution/build.gradle.kts b/appdistribution/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/appdistribution/build.gradle.kts +++ b/appdistribution/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/auth/app/build.gradle.kts b/auth/app/build.gradle.kts index 2bd1161127..f622c91e86 100644 --- a/auth/app/build.gradle.kts +++ b/auth/app/build.gradle.kts @@ -54,15 +54,15 @@ dependencies { implementation(project(":internal:lintchecks")) implementation("androidx.multidex:multidex:2.0.1") - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation("androidx.vectordrawable:vectordrawable-animated:1.2.0") implementation("com.google.android.material:material:1.13.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.9.5") - implementation("androidx.navigation:navigation-ui-ktx:2.9.5") + implementation("androidx.navigation:navigation-fragment-ktx:2.9.6") + implementation("androidx.navigation:navigation-ui-ktx:2.9.6") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Authentication implementation("com.google.firebase:firebase-auth") @@ -74,7 +74,7 @@ dependencies { // Firebase UI // Used in FirebaseUIActivity. - implementation("com.firebaseui:firebase-ui-auth:9.0.0") + implementation("com.firebaseui:firebase-ui-auth:9.1.1") // Facebook Android SDK (only required for Facebook Login) // Used in FacebookLoginActivity. diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/build.gradle.kts b/build.gradle.kts index f548ae3e58..915208b38b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,15 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false id("com.google.firebase.crashlytics") version "3.0.6" apply false - id("com.google.firebase.firebase-perf") version "2.0.1" apply false - id("androidx.navigation.safeargs") version "2.9.5" apply false + id("com.google.firebase.firebase-perf") version "2.0.2" apply false + id("androidx.navigation.safeargs") version "2.9.6" apply false id("com.github.ben-manes.versions") version "0.53.0" apply true - id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" apply false } allprojects { diff --git a/config/app/build.gradle.kts b/config/app/build.gradle.kts index f4a9924500..09ddbca120 100644 --- a/config/app/build.gradle.kts +++ b/config/app/build.gradle.kts @@ -53,7 +53,7 @@ dependencies { implementation("com.google.android.material:material:1.13.0") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Remote Config implementation("com.google.firebase:firebase-config") diff --git a/config/build.gradle.kts b/config/build.gradle.kts index c07891342a..06b429a6b4 100644 --- a/config/build.gradle.kts +++ b/config/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/crash/app/build.gradle.kts b/crash/app/build.gradle.kts index ce7cc1c890..3e3bdc6e01 100644 --- a/crash/app/build.gradle.kts +++ b/crash/app/build.gradle.kts @@ -57,10 +57,10 @@ dependencies { implementation(project(":internal:lintchecks")) implementation(project(":internal:chooserx")) implementation("com.google.android.material:material:1.13.0") - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Crashlytics implementation("com.google.firebase:firebase-crashlytics") diff --git a/crash/build.gradle.kts b/crash/build.gradle.kts index 128042bfff..a0c5224c67 100644 --- a/crash/build.gradle.kts +++ b/crash/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false id("com.google.firebase.crashlytics") version "3.0.6" apply false } diff --git a/database/app/build.gradle.kts b/database/app/build.gradle.kts index 7b1b963d1e..84c8a3725d 100644 --- a/database/app/build.gradle.kts +++ b/database/app/build.gradle.kts @@ -54,11 +54,11 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.recyclerview:recyclerview:1.4.0") implementation("com.google.android.material:material:1.13.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.9.5") - implementation("androidx.navigation:navigation-ui-ktx:2.9.5") + implementation("androidx.navigation:navigation-fragment-ktx:2.9.6") + implementation("androidx.navigation:navigation-ui-ktx:2.9.6") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Realtime Database implementation("com.google.firebase:firebase-database") @@ -66,7 +66,7 @@ dependencies { // Firebase Authentication implementation("com.google.firebase:firebase-auth") - implementation("com.firebaseui:firebase-ui-database:9.0.0") + implementation("com.firebaseui:firebase-ui-database:9.1.1") // Needed to fix a dependency conflict with FirebaseUI' implementation("androidx.arch.core:core-runtime:2.2.0") diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/firebase-ai/app/build.gradle.kts b/firebase-ai/app/build.gradle.kts index f350bc2ad4..b070d8906d 100644 --- a/firebase-ai/app/build.gradle.kts +++ b/firebase-ai/app/build.gradle.kts @@ -67,6 +67,13 @@ dependencies { // Webkit implementation(libs.androidx.webkit) + // CameraX (for video with the Gemini Live API) + implementation(libs.androidx.camera.core) + implementation(libs.androidx.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.view) + implementation(libs.androidx.camera.extensions) + // Material for XML-based theme implementation(libs.material) @@ -74,6 +81,11 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.ai) + // Image loading + implementation(libs.coil3.coil.compose) + implementation(libs.coil.network.okhttp) + implementation(libs.coil.svg) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/firebase-ai/app/src/main/AndroidManifest.xml b/firebase-ai/app/src/main/AndroidManifest.xml index 699c61714c..bad5b1aa6b 100644 --- a/firebase-ai/app/src/main/AndroidManifest.xml +++ b/firebase-ai/app/src/main/AndroidManifest.xml @@ -6,6 +6,11 @@ + + + + + - \ No newline at end of file + diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index ad3a5cd435..381bc30ea4 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt @@ -8,6 +8,7 @@ import com.google.firebase.ai.type.Schema import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.content import com.google.firebase.ai.type.generationConfig +import com.google.firebase.ai.type.thinkingConfig import com.google.firebase.quickstart.ai.feature.media.imagen.EditingMode import com.google.firebase.quickstart.ai.ui.navigation.Category import com.google.firebase.quickstart.ai.ui.navigation.Sample @@ -126,8 +127,8 @@ val FIREBASE_AI_SAMPLES = listOf( } ), Sample( - title = "Imagen 3 - image generation", - description = "Generate images using Imagen 3", + title = "Imagen 4 - image generation", + description = "Generate images using Imagen 4", navRoute = "imagen", categories = listOf(Category.IMAGE), initialPrompt = content { @@ -298,6 +299,34 @@ val FIREBASE_AI_SAMPLES = listOf( text("What was the weather in Boston, MA on October 17, 2024?") } ), + Sample( + title = "Gemini Live (Video input)", + description = "Use bidirectional streaming to chat with Gemini using your" + + " phone's camera", + navRoute = "streamVideo", + categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING), + tools = listOf( + Tool.functionDeclarations( + listOf( + FunctionDeclaration( + "fetchWeather", + "Get the weather conditions for a specific US city on a specific date.", + mapOf( + "city" to Schema.string("The US city of the location."), + "state" to Schema.string("The US state of the location."), + "date" to Schema.string( + "The date for which to get the weather." + + " Date must be in the format: YYYY-MM-DD." + ), + ), + ) + ) + ) + ), + initialPrompt = content { + text("What was the weather in Boston, MA on October 17, 2024?") + } + ), Sample( title = "Weather Chat", description = "Use function calling to get the weather conditions" + @@ -328,7 +357,8 @@ val FIREBASE_AI_SAMPLES = listOf( ), Sample( title = "Grounding with Google Search", - description = "Use Grounding with Google Search to get responses based on up-to-date information from the web.", + description = "Use Grounding with Google Search to get responses based on up-to-date information from the" + + " web.", navRoute = "chat", categories = listOf(Category.TEXT, Category.DOCUMENT), modelName = "gemini-2.5-flash", @@ -339,4 +369,53 @@ val FIREBASE_AI_SAMPLES = listOf( ) }, ), + Sample( + title = "Server Prompt Template - Imagen", + description = "Generate an image using a server prompt template. Note that you need to setup the template in" + + "the Firebase console before running this demo.", + navRoute = "imagen", + categories = listOf(Category.IMAGE), + initialPrompt = content { text("List of things that should be in the image") }, + allowEmptyPrompt = false, + editingMode = EditingMode.TEMPLATE, + // To make this work, create an "Imagen (Basic)" server prompt template in your Firebase project with this name + templateId = "imagen-basic", + templateKey = "prompt" + ), + Sample( + title = "Server Prompt Templates - Gemini", + description = "Generate an invoice using server prompt templates. Note that you need to setup the template" + + " in the Firebase console before running this demo.", + navRoute = "text", + categories = listOf(Category.TEXT), + initialPrompt = content { text("Jane Doe") }, + allowEmptyPrompt = false, + // To make this work, create an `Input + System Instructions` template in your Firebase project with this name + templateId = "input-system-instructions", + templateKey = "customerName" + ), + Sample( + title = "SVG Generator", + description = "Use Gemini 3 Flash to create SVG illustrations", + navRoute = "svg", + categories = listOf(Category.IMAGE, Category.TEXT), + initialPrompt = content { + text( + "a kitten" + ) + }, + modelName = "gemini-3-flash-preview", + generationConfig = generationConfig { + thinkingConfig { + thinkingBudget = -1 + } + }, + systemInstructions = content { text(""" + You are an expert at turning image prompts into SVG code. When given a prompt, + use your creativity to code a 800x600 SVG rendering of it. + Always add viewBox="0 0 800 600" to the root svg tag. Do + not import external assets, they won't work. Return ONLY the SVG code, nothing else, + no commentary. + """.trimIndent()) } + ), ) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index 54eaff6543..8ab79f9876 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -1,6 +1,7 @@ package com.google.firebase.quickstart.ai import android.Manifest +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -31,10 +32,16 @@ import androidx.navigation.compose.rememberNavController import com.google.firebase.ai.type.toImagenInlineImage import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeScreen +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoScreen import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenRoute import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenScreen +import com.google.firebase.quickstart.ai.feature.svg.SvgRoute +import com.google.firebase.quickstart.ai.feature.svg.SvgScreen import com.google.firebase.quickstart.ai.feature.text.ChatRoute import com.google.firebase.quickstart.ai.feature.text.ChatScreen +import com.google.firebase.quickstart.ai.feature.text.TextGenRoute +import com.google.firebase.quickstart.ai.feature.text.TextGenScreen import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen import com.google.firebase.quickstart.ai.ui.theme.FirebaseAILogicTheme @@ -42,10 +49,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if(ContextCompat.checkSelfPermission(this, - Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } + enableEdgeToEdge() catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat) setContent { @@ -90,6 +94,15 @@ class MainActivity : ComponentActivity() { "stream" -> { navController.navigate(StreamRealtimeRoute(it.id)) } + "streamVideo" -> { + navController.navigate(StreamRealtimeVideoRoute(it.id)) + } + "text" -> { + navController.navigate(TextGenRoute(it.id)) + } + "svg" -> { + navController.navigate(SvgRoute(it.id)) + } } } ) @@ -102,10 +115,24 @@ class MainActivity : ComponentActivity() { composable { ImagenScreen() } - // Stream Realtime Samples + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeScreen composable. + @SuppressLint("MissingPermission") composable { StreamRealtimeScreen() } + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeVideoScreen composable. + @SuppressLint("MissingPermission") + composable { + StreamRealtimeVideoScreen() + } + composable { + TextGenScreen() + } + composable { + SvgScreen() + } } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index a7f183704f..80589b18f0 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -1,56 +1,33 @@ -package com.google.firebase.quickstart.ai.feature.media.imagen +package com.google.firebase.quickstart.ai.feature.live -import android.Manifest +import android.annotation.SuppressLint import android.graphics.Bitmap -import androidx.annotation.RequiresPermission import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.google.firebase.Firebase import com.google.firebase.ai.FirebaseAI -import com.google.firebase.ai.ImagenModel -import com.google.firebase.ai.LiveGenerativeModel -import com.google.firebase.ai.ai import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart -import com.google.firebase.ai.type.GenerativeBackend -import com.google.firebase.ai.type.ImagenAspectRatio -import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.ImagenPersonFilterLevel -import com.google.firebase.ai.type.ImagenSafetyFilterLevel -import com.google.firebase.ai.type.ImagenSafetySettings -import com.google.firebase.ai.type.InlineDataPart -import com.google.firebase.ai.type.LiveServerContent -import com.google.firebase.ai.type.LiveServerMessage +import com.google.firebase.ai.type.InlineData import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseModality import com.google.firebase.ai.type.SpeechConfig -import com.google.firebase.ai.type.TextPart -import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.Voice -import com.google.firebase.ai.type.asTextOrNull -import com.google.firebase.ai.type.imagenGenerationConfig import com.google.firebase.ai.type.liveGenerationConfig import com.google.firebase.app import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES -import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute -import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import java.io.ByteArrayOutputStream import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive @OptIn(PublicPreviewAPI::class) -class BidiViewModel( - savedStateHandle: SavedStateHandle -) : ViewModel() { +class BidiViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val sampleId = savedStateHandle.toRoute().sampleId private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId } @@ -63,41 +40,56 @@ class BidiViewModel( // Change this to ContentModality.TEXT if you want text output. responseModality = ResponseModality.AUDIO } + @OptIn(PublicPreviewAPI::class) - val liveModel = FirebaseAI.getInstance(Firebase.app, sample.backend).liveModel( - "gemini-live-2.5-flash", - generationConfig = liveGenerationConfig, - tools = sample.tools - ) - runBlocking { - liveSession = liveModel.connect() - } + val liveModel = + FirebaseAI.getInstance(Firebase.app, sample.backend) + .liveModel( + // If you are using Vertex AI, change the model name to + // "gemini-live-2.5-flash-preview-native-audio-09-2025" + modelName = sample.modelName ?: "gemini-2.5-flash-native-audio-preview-09-2025", + generationConfig = liveGenerationConfig, + tools = sample.tools, + ) + runBlocking { liveSession = liveModel.connect() } } - fun handler(fetchWeatherCall: FunctionCallPart) : FunctionResponsePart { - val response:JsonObject + fun handler(fetchWeatherCall: FunctionCallPart): FunctionResponsePart { + val response: JsonObject fetchWeatherCall.let { val city = it.args["city"]?.jsonPrimitive?.content val state = it.args["state"]?.jsonPrimitive?.content val date = it.args["date"]?.jsonPrimitive?.content runBlocking { - response = if(!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { - fetchWeather(city!!, state!!, date!!) - } else { - JsonObject(emptyMap()) - } + response = + if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and !date.isNullOrEmpty()) { + fetchWeather(city!!, state!!, date!!) + } else { + JsonObject(emptyMap()) + } } } - return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) + return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } - @RequiresPermission(Manifest.permission.RECORD_AUDIO) + + // The permission check is handled by the view that calls this function. + @SuppressLint("MissingPermission") suspend fun startConversation() { - liveSession.startAudioConversation(::handler) + liveSession.startAudioConversation(::handler) } fun endConversation() { liveSession.stopAudioConversation() } + fun sendVideoFrame(frame: Bitmap) { + viewModelScope.launch { + // Directly compress the Bitmap to a ByteArray + val byteArrayOutputStream = ByteArrayOutputStream() + frame.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream) + val jpegBytes = byteArrayOutputStream.toByteArray() + liveSession.sendVideoRealtime(InlineData(jpegBytes, "image/jpeg")) + } + } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt new file mode 100644 index 0000000000..8fcf0258ba --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -0,0 +1,98 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import kotlin.time.Duration.Companion.seconds + +@Composable +fun CameraView( + modifier: Modifier = Modifier, + cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, + onFrameCaptured: (Bitmap) -> Unit, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + + AndroidView( + factory = { ctx -> + val previewView = PreviewView(ctx) + val executor = ContextCompat.getMainExecutor(ctx) + cameraProviderFuture.addListener( + { + val cameraProvider = cameraProviderFuture.get() + bindPreview( + lifecycleOwner, + previewView, + cameraProvider, + cameraSelector, + onFrameCaptured, + ) + }, + executor, + ) + previewView + }, + modifier = modifier, + ) +} + +private fun bindPreview( + lifecycleOwner: LifecycleOwner, + previewView: PreviewView, + cameraProvider: ProcessCameraProvider, + cameraSelector: CameraSelector, + onFrameCaptured: (Bitmap) -> Unit, +) { + val preview = + Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } + + val imageAnalysis = + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer( + ContextCompat.getMainExecutor(previewView.context), + SnapshotFrameAnalyzer(onFrameCaptured), + ) + } + + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) +} + +// Calls the [onFrameCaptured] callback with the captured frame every second. +private class SnapshotFrameAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : + ImageAnalysis.Analyzer { + private var lastFrameTimestamp = 0L + private val interval = 1.seconds // 1 second + + @SuppressLint("UnsafeOptInUsageError") + override fun analyze(image: ImageProxy) { + val currentTimestamp = System.currentTimeMillis() + if (lastFrameTimestamp == 0L) { + lastFrameTimestamp = currentTimestamp + } + + if (currentTimestamp - lastFrameTimestamp >= interval.inWholeMilliseconds) { + onFrameCaptured(image.toBitmap()) + lastFrameTimestamp = currentTimestamp + } + image.close() + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt index f088cda23b..194b04023f 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt @@ -32,8 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt new file mode 100644 index 0000000000..a30c93980c --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -0,0 +1,82 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.Manifest +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresPermission +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable class StreamRealtimeVideoRoute(val sampleId: String) + +@RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA]) +@Composable +fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { + val backgroundColor = MaterialTheme.colorScheme.background + + val scope = rememberCoroutineScope() + + val context = LocalContext.current + var hasPermissions by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + ) + } + + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + permissions -> + hasPermissions = permissions.values.all { it } + } + + LaunchedEffect(Unit) { + if (!hasPermissions) { + launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) + } + } + + DisposableEffect(hasPermissions) { + if (hasPermissions) { + scope.launch { bidiView.startConversation() } + } + onDispose { bidiView.endConversation() } + } + + Surface(modifier = Modifier.fillMaxSize(), color = backgroundColor) { + Column(modifier = Modifier.fillMaxSize()) { + if (hasPermissions) { + Box(modifier = Modifier.fillMaxSize()) { + CameraView( + modifier = Modifier.fillMaxHeight(0.5f), + onFrameCaptured = { bitmap -> bidiView.sendVideoFrame(bitmap) }, + ) + } + } else { + Text("Camera and audio permissions are required to use this feature.") + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/EditingMode.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/EditingMode.kt index 6e997092d6..10093e7350 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/EditingMode.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/EditingMode.kt @@ -6,4 +6,5 @@ enum class EditingMode { OUTPAINTING, SUBJECT_REFERENCE, STYLE_TRANSFER, + TEMPLATE, } \ No newline at end of file diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt index 8b6f223a6f..cf794a0368 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import androidx.core.graphics.scale +import com.google.firebase.ai.TemplateImagenModel import com.google.firebase.ai.type.Dimensions import com.google.firebase.ai.type.ImagenBackgroundMask import com.google.firebase.ai.type.ImagenEditMode @@ -67,6 +68,10 @@ class ImagenViewModel( val additionalImage = sample.additionalImage + val templateId = sample.templateId + + val templateKey = sample.templateKey + private val _attachedImage = MutableStateFlow(null) val attachedImage: StateFlow = _attachedImage @@ -75,6 +80,7 @@ class ImagenViewModel( // Firebase AI Logic private val imagenModel: ImagenModel + private val templateImagenModel: TemplateImagenModel init { val config = imagenGenerationConfig { @@ -88,27 +94,38 @@ class ImagenViewModel( imagenModel = Firebase.ai( backend = sample.backend ).imagenModel( - modelName = sample.modelName ?: "imagen-3.0-generate-002", + modelName = sample.modelName ?: "imagen-4.0-generate-001", generationConfig = config, safetySettings = settings ) + templateImagenModel = Firebase.ai.templateImagenModel() } fun generateImages(inputText: String) { viewModelScope.launch { _isLoading.value = true + _errorMessage.value = null // clear error message try { val imageResponse = when(sample.editingMode) { EditingMode.INPAINTING -> inpaint(imagenModel, inputText) EditingMode.OUTPAINTING -> outpaint(imagenModel, inputText) EditingMode.SUBJECT_REFERENCE -> drawReferenceSubject(imagenModel, inputText) EditingMode.STYLE_TRANSFER -> transferStyle(imagenModel, inputText) + EditingMode.TEMPLATE -> + generateWithTemplate(templateImagenModel, templateId!!, mapOf(templateKey!! to inputText)) else -> generate(imagenModel, inputText) } _generatedBitmaps.value = imageResponse.images.map { it.asBitmap() } - _errorMessage.value = null // clear error message } catch (e: Exception) { - _errorMessage.value = e.localizedMessage + val errorMessage = + if ((e.localizedMessage?.contains("not found") == true) && + sample.editingMode == EditingMode.TEMPLATE) { + "Template was not found, please verify that your project contains a" + + " template named \"$templateId\"." + } else { + e.localizedMessage + } + _errorMessage.value = errorMessage } finally { _isLoading.value = false } @@ -212,4 +229,12 @@ class ImagenViewModel( inputText ) } + + suspend fun generateWithTemplate( + model: TemplateImagenModel, + templateId: String, + inputMap: Map + ): ImagenGenerationResponse { + return model.generateImages(templateId, inputMap) + } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgScreen.kt new file mode 100644 index 0000000000..be745faec2 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgScreen.kt @@ -0,0 +1,131 @@ +package com.google.firebase.quickstart.ai.feature.svg + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.compose.SubcomposeAsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import coil3.svg.SvgDecoder +import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.Serializable +import java.nio.ByteBuffer + +@Serializable +class SvgRoute(val sampleId: String) + +@Composable +fun SvgScreen( + svgViewModel: SvgViewModel = viewModel() +) { + var prompt by rememberSaveable { mutableStateOf(svgViewModel.initialPrompt) } + val errorMessage by svgViewModel.errorMessage.collectAsStateWithLifecycle() + val isLoading by svgViewModel.isLoading.collectAsStateWithLifecycle() + val generatedSvgs by svgViewModel.generatedSvgs.collectAsStateWithLifecycle() + + Column { + ElevatedCard( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large + ) { + OutlinedTextField( + value = prompt, + label = { Text("Generate a SVG of") }, + placeholder = { Text("Enter text to generate image") }, + onValueChange = { prompt = it }, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) + TextButton( + onClick = { + svgViewModel.generateSVG(prompt) + }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 16.dp) + .align(Alignment.End) + ) { + Text("Generate") + } + } + if (isLoading) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(all = 8.dp) + .align(Alignment.CenterHorizontally) + ) { + CircularProgressIndicator() + } + } + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(generatedSvgs) { svg -> + Card( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + ) { + SubcomposeAsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(ByteBuffer.wrap(svg.toByteArray())) + .decoderFactory(SvgDecoder.Factory()) + .decoderCoroutineContext(Dispatchers.Main) + .crossfade(true) + .build(), + contentDescription = "Generated SVG", + modifier = Modifier + .fillMaxWidth() + ) + } + } + } + errorMessage?.let { + Card( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(all = 16.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt new file mode 100644 index 0000000000..71e69fbf7e --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt @@ -0,0 +1,72 @@ +package com.google.firebase.quickstart.ai.feature.svg + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.google.firebase.Firebase +import com.google.firebase.ai.GenerativeModel +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.TextPart +import com.google.firebase.ai.type.asTextOrNull +import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES +import com.google.firebase.quickstart.ai.feature.text.ChatRoute +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class SvgViewModel( + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val sampleId = savedStateHandle.toRoute().sampleId + private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId } + val initialPrompt: String = + sample.initialPrompt?.parts + ?.filterIsInstance() + ?.first() + ?.asTextOrNull().orEmpty() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading + + private val _errorMessage = MutableStateFlow(null) + val errorMessage: StateFlow = _errorMessage + + private val _generatedSvgList = mutableStateListOf() + val generatedSvgs: StateFlow> = + MutableStateFlow>(_generatedSvgList) + + private val generativeModel: GenerativeModel + + init { + generativeModel = Firebase.ai( + backend = GenerativeBackend.vertexAI("global") // GenerativeBackend.googleAI() by default + ).generativeModel( + modelName = sample.modelName ?: "gemini-2.5-flash", + systemInstruction = sample.systemInstructions, + generationConfig = sample.generationConfig, + tools = sample.tools + ) + } + + fun generateSVG(prompt: String) { + _isLoading.value = true + viewModelScope.launch(Dispatchers.IO) { + try { + val response = generativeModel.generateContent(prompt) + response.text?.let { + _generatedSvgList.addFirst(it) + } + _errorMessage.value = null + } catch (e: Exception) { + _errorMessage.value = e.localizedMessage + } finally { + _isLoading.value = false + } + } + + } +} \ No newline at end of file diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt index 276024c27f..f89bcba17a 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.Bitmap import android.net.Uri +import androidx.core.net.toUri import android.provider.OpenableColumns import android.text.format.Formatter import android.webkit.WebResourceRequest @@ -374,7 +375,7 @@ fun SourceLinkView( ClickableText(text = annotatedString, onClick = { offset -> annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))) + context.startActivity(Intent(Intent.ACTION_VIEW, annotation.item.toUri())) } }) } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt index 33afaad2ee..f70a0d7f41 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt @@ -72,9 +72,10 @@ class ChatViewModel( init { val generativeModel = Firebase.ai( - backend = sample.backend // GenerativeBackend.googleAI() by default + backend = GenerativeBackend.vertexAI("global") // GenerativeBackend.googleAI() by default ).generativeModel( - modelName = sample.modelName ?: "gemini-2.5-flash", +// modelName = sample.modelName ?: "gemini-3-pro-preview-testing-only", + modelName = sample.modelName ?: "gemini-2.5-pro", systemInstruction = sample.systemInstructions, generationConfig = sample.generationConfig, tools = sample.tools diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenScreen.kt new file mode 100644 index 0000000000..26331333bf --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenScreen.kt @@ -0,0 +1,142 @@ +package com.google.firebase.quickstart.ai.feature.text + +import android.net.Uri +import android.provider.OpenableColumns +import android.text.format.Formatter +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.firebase.quickstart.ai.R +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +class TextGenRoute(val sampleId: String) + +@Composable +fun TextGenScreen( + textGenViewModel: TextGenViewModel = viewModel() +) { + var textPrompt by rememberSaveable { mutableStateOf(textGenViewModel.initialPrompt) } + val errorMessage by textGenViewModel.errorMessage.collectAsStateWithLifecycle() + val isLoading by textGenViewModel.isLoading.collectAsStateWithLifecycle() + val generatedText by textGenViewModel.generatedText.collectAsStateWithLifecycle() + + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + ElevatedCard( + modifier = Modifier + .padding(all = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large + ) { + OutlinedTextField( + value = textPrompt, + label = { Text("Prompt") }, + placeholder = { Text("Enter text to generate") }, + onValueChange = { textPrompt = it }, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) + Row() { + TextButton( + onClick = { + if (textGenViewModel.allowEmptyPrompt || textPrompt.isNotBlank()) { + textGenViewModel.generate(textPrompt) + } + }, + modifier = Modifier.padding(end = 16.dp, bottom = 16.dp) + ) { + Text("Generate") + } + } + + } + + if (isLoading) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(all = 8.dp) + .align(Alignment.CenterHorizontally) + ) { + CircularProgressIndicator() + } + } + errorMessage?.let { + Card( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(all = 16.dp) + ) + } + } + generatedText?.let { + Card( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Text( + text = it, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(all = 16.dp) + ) + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenViewModel.kt new file mode 100644 index 0000000000..52532daef2 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TextGenViewModel.kt @@ -0,0 +1,83 @@ +package com.google.firebase.quickstart.ai.feature.text + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.google.firebase.Firebase +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.asTextOrNull +import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import com.google.firebase.ai.GenerativeModel +import com.google.firebase.ai.TemplateGenerativeModel + +@OptIn(PublicPreviewAPI::class) +class TextGenViewModel( + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val sampleId = savedStateHandle.toRoute().sampleId + private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId } + val initialPrompt = sample.initialPrompt?.parts?.first()?.asTextOrNull().orEmpty() + + private val _errorMessage: MutableStateFlow = MutableStateFlow(null) + val errorMessage: StateFlow = _errorMessage + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading + + val allowEmptyPrompt = sample.allowEmptyPrompt + + val templateId = sample.templateId + + val templateKey = sample.templateKey + + private val _generatedText = MutableStateFlow(null) + val generatedText: StateFlow = _generatedText + + // Firebase AI Logic + private val generativeModel: GenerativeModel + private val templateGenerativeModel: TemplateGenerativeModel + + init { + generativeModel = Firebase.ai( + backend = sample.backend // GenerativeBackend.googleAI() by default + ).generativeModel( + modelName = sample.modelName ?: "gemini-2.5-flash", + systemInstruction = sample.systemInstructions, + generationConfig = sample.generationConfig, + tools = sample.tools + ) + templateGenerativeModel = Firebase.ai.templateGenerativeModel() + } + + fun generate(inputText: String) { + viewModelScope.launch { + _isLoading.value = true + _errorMessage.value = null // clear error message + try { + val generativeResponse = if (templateId != null) { + templateGenerativeModel + .generateContent(templateId, mapOf(templateKey!! to inputText)) + } else { + generativeModel.generateContent(inputText) + } + _generatedText.value = generativeResponse.text + } catch (e: Exception) { + val errorMessage = + if ((e.localizedMessage?.contains("not found") == true) && (templateId != null)) { + "Template was not found, please verify that your project contains a" + + " template named \"$templateId\"." + } else { + e.localizedMessage + } + _errorMessage.value = errorMessage + } finally { + _isLoading.value = false + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt index ad0cccbecf..3704b2b449 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt @@ -45,4 +45,6 @@ data class Sample( val imageLabels: List = emptyList(), val selectionOptions: List = emptyList(), val editingMode: EditingMode? = null, + val templateId: String? = null, + val templateKey: String? = null, ) diff --git a/firebase-ai/app/src/main/res/values/colors.xml b/firebase-ai/app/src/main/res/values/colors.xml index f8c6127d32..55344e5192 100644 --- a/firebase-ai/app/src/main/res/values/colors.xml +++ b/firebase-ai/app/src/main/res/values/colors.xml @@ -1,10 +1,3 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF \ No newline at end of file diff --git a/firebase-ai/build.gradle.kts b/firebase-ai/build.gradle.kts index aa7d6f1011..5af44bffd0 100644 --- a/firebase-ai/build.gradle.kts +++ b/firebase-ai/build.gradle.kts @@ -1,10 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false - id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false - id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false + id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/firebase-ai/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml index 49abf9fe73..db9d5ed20d 100644 --- a/firebase-ai/gradle/libs.versions.toml +++ b/firebase-ai/gradle/libs.versions.toml @@ -1,19 +1,20 @@ [versions] -activityCompose = "1.11.0" -agp = "8.9.2" -composeBom = "2024.09.00" -composeNavigation = "2.9.5" +activityCompose = "1.12.1" +agp = "8.13.0" +composeBom = "2025.10.00" +composeNavigation = "2.9.6" coreKtx = "1.17.0" espressoCore = "3.7.0" -firebaseBom = "34.4.0" +firebaseBom = "34.7.0" junit = "4.13.2" junitVersion = "1.3.0" -kotlin = "2.0.21" +kotlin = "2.2.20" kotlinxSerializationCore = "1.9.0" -lifecycle = "2.9.4" -lifecycleRuntimeKtx = "2.8.7" +lifecycle = "2.10.0" +lifecycleRuntimeKtx = "2.9.4" material = "1.13.0" webkit = "1.14.0" +camerax = "1.5.2" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -41,6 +42,11 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/firebase-ai/settings.gradle.kts b/firebase-ai/settings.gradle.kts index f1cb15d710..26668b9361 100644 --- a/firebase-ai/settings.gradle.kts +++ b/firebase-ai/settings.gradle.kts @@ -14,6 +14,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() } diff --git a/firestore/app/build.gradle.kts b/firestore/app/build.gradle.kts index eab5daa7e9..75b4562f90 100644 --- a/firestore/app/build.gradle.kts +++ b/firestore/app/build.gradle.kts @@ -58,7 +58,7 @@ dependencies { implementation(project(":internal:chooserx")) // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firestore implementation("com.google.firebase:firebase-firestore") @@ -71,10 +71,10 @@ dependencies { implementation("com.google.android.gms:play-services-auth:20.7.0") // FirebaseUI (for authentication) - implementation("com.firebaseui:firebase-ui-auth:9.0.0") + implementation("com.firebaseui:firebase-ui-auth:9.1.1") // Support Libs - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.core:core-ktx:1.17.0") implementation("androidx.vectordrawable:vectordrawable-animated:1.2.0") @@ -84,13 +84,13 @@ dependencies { implementation("androidx.media:media:1.7.1") implementation("androidx.recyclerview:recyclerview:1.4.0") implementation("androidx.multidex:multidex:2.0.1") - implementation("androidx.navigation:navigation-fragment-ktx:2.9.5") - implementation("androidx.navigation:navigation-ui-ktx:2.9.5") + implementation("androidx.navigation:navigation-fragment-ktx:2.9.6") + implementation("androidx.navigation:navigation-ui-ktx:2.9.6") // Android architecture components - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.9.4") + annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.10.0") // Third-party libraries implementation("me.zhanghai.android.materialratingbar:library:1.4.0") diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 607aed6e7c..06fed104ce 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -1,11 +1,11 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false - id("androidx.navigation.safeargs") version "2.9.5" apply false + id("androidx.navigation.safeargs") version "2.9.6" apply false } allprojects { diff --git a/functions/app/build.gradle.kts b/functions/app/build.gradle.kts index 26b425ed47..6d09158dc1 100644 --- a/functions/app/build.gradle.kts +++ b/functions/app/build.gradle.kts @@ -49,13 +49,13 @@ dependencies { implementation(project(":internal:lintchecks")) implementation(project(":internal:chooserx")) - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") implementation("androidx.fragment:fragment-ktx:1.8.9") implementation("androidx.appcompat:appcompat:1.7.1") implementation("com.google.android.material:material:1.13.0") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Cloud Functions for Firebase implementation("com.google.firebase:firebase-functions") @@ -67,7 +67,7 @@ dependencies { implementation("com.google.firebase:firebase-messaging") // Firebase UI - implementation("com.firebaseui:firebase-ui-auth:9.0.0") + implementation("com.firebaseui:firebase-ui-auth:9.1.1") // Google Play services implementation("com.google.android.gms:play-services-auth:21.2.0") diff --git a/functions/build.gradle.kts b/functions/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/functions/build.gradle.kts +++ b/functions/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/functions/functions/package-lock.json b/functions/functions/package-lock.json index 7259e81915..da3d2f0bd4 100644 --- a/functions/functions/package-lock.json +++ b/functions/functions/package-lock.json @@ -12,7 +12,7 @@ "firebase-functions": "^6.1.0" }, "engines": { - "node": "^22" + "node": "20" } }, "node_modules/@fastify/busboy": { @@ -1109,6 +1109,7 @@ "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "1.0.8", @@ -1576,34 +1577,34 @@ } }, "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "optional": true, "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -1626,13 +1627,13 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "optional": true, "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -1821,9 +1822,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebeda0ad79..58ba2369a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,49 +1,59 @@ [versions] -agp = "8.13.0" +activityCompose = "1.12.1" +agp = "8.13.2" +camerax = "1.5.2" coilCompose = "2.7.0" -firebaseBom = "34.4.0" -kotlin = "2.2.20" +composeBom = "2025.12.00" +composeNavigation = "2.9.6" +coil3Compose = "3.3.0" coreKtx = "1.17.0" +espressoCore = "3.7.0" +firebaseBom = "34.7.0" +googleServices = "4.4.4" junit = "4.13.2" junitVersion = "1.3.0" -espressoCore = "3.7.0" +kotlin = "2.2.21" kotlinxSerializationCore = "1.9.0" -lifecycle = "2.9.4" -activityCompose = "1.11.0" -composeBom = "2025.10.01" -googleServices = "4.4.4" -composeNavigation = "2.9.5" +lifecycle = "2.10.0" material = "1.13.0" webkit = "1.14.0" [libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-lifecycle-runtime-compose-android = { module = "androidx.lifecycle:lifecycle-runtime-compose-android", version.ref = "lifecycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } -coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } -firebase-ai = { module = "com.google.firebase:firebase-ai" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3Compose" } +coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3Compose" } +coil3-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3Compose" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"} +firebase-ai = { module = "com.google.firebase:firebase-ai" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } -androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } material = { module = "com.google.android.material:material", version.ref = "material" } [plugins] diff --git a/inappmessaging/app/build.gradle.kts b/inappmessaging/app/build.gradle.kts index a574d5e535..550b2ee9fe 100644 --- a/inappmessaging/app/build.gradle.kts +++ b/inappmessaging/app/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { implementation("androidx.multidex:multidex:2.0.1") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // FIAM implementation("com.google.firebase:firebase-inappmessaging-display") diff --git a/inappmessaging/build.gradle.kts b/inappmessaging/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/inappmessaging/build.gradle.kts +++ b/inappmessaging/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/internal/lint/build.gradle.kts b/internal/lint/build.gradle.kts index 1b154e9659..2bdad9dfb0 100755 --- a/internal/lint/build.gradle.kts +++ b/internal/lint/build.gradle.kts @@ -9,8 +9,8 @@ java { } dependencies { - compileOnly("com.android.tools.lint:lint-api:31.13.0") - testImplementation("com.android.tools.lint:lint:31.13.0") - testImplementation("com.android.tools.lint:lint-tests:31.13.0") + compileOnly("com.android.tools.lint:lint-api:31.13.2") + testImplementation("com.android.tools.lint:lint:31.13.2") + testImplementation("com.android.tools.lint:lint-tests:31.13.2") testImplementation("junit:junit:4.13.2") } diff --git a/messaging/app/build.gradle.kts b/messaging/app/build.gradle.kts index cf3ce5b67c..fb4bcf9c44 100644 --- a/messaging/app/build.gradle.kts +++ b/messaging/app/build.gradle.kts @@ -61,13 +61,13 @@ dependencies { implementation("androidx.core:core-ktx:1.17.0") // Required when asking for permission to post notifications (starting in Android 13) - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") implementation("androidx.fragment:fragment-ktx:1.8.9") implementation("com.google.android.material:material:1.13.0") // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Cloud Messaging implementation("com.google.firebase:firebase-messaging") diff --git a/messaging/build.gradle.kts b/messaging/build.gradle.kts index c07891342a..06b429a6b4 100644 --- a/messaging/build.gradle.kts +++ b/messaging/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } diff --git a/perf/app/build.gradle.kts b/perf/app/build.gradle.kts index 54e4d83678..e04827406d 100644 --- a/perf/app/build.gradle.kts +++ b/perf/app/build.gradle.kts @@ -64,14 +64,14 @@ dependencies { implementation(project(":internal:chooserx")) // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Firebase Performance Monitoring implementation("com.google.firebase:firebase-perf") implementation("com.google.android.material:material:1.13.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0") implementation("com.github.bumptech.glide:glide:4.12.0") diff --git a/perf/build.gradle.kts b/perf/build.gradle.kts index 3b86946a3c..9d241429eb 100644 --- a/perf/build.gradle.kts +++ b/perf/build.gradle.kts @@ -1,11 +1,11 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false - id("com.google.firebase.firebase-perf") version "2.0.1" apply false + id("com.google.firebase.firebase-perf") version "2.0.2" apply false } allprojects { diff --git a/storage/app/build.gradle.kts b/storage/app/build.gradle.kts index 3b2c99a62a..476b42161e 100644 --- a/storage/app/build.gradle.kts +++ b/storage/app/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(project(":internal:chooserx")) // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom) - implementation(platform("com.google.firebase:firebase-bom:34.4.0")) + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) // Cloud Storage for Firebase implementation("com.google.firebase:firebase-storage") @@ -59,7 +59,7 @@ dependencies { // Firebase Authentication implementation("com.google.firebase:firebase-auth") - implementation("androidx.activity:activity-ktx:1.11.0") + implementation("androidx.activity:activity-ktx:1.12.1") implementation("androidx.appcompat:appcompat:1.7.1") implementation("com.google.android.material:material:1.13.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts index 8eb799a20a..6be8af03ed 100644 --- a/storage/build.gradle.kts +++ b/storage/build.gradle.kts @@ -1,9 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.13.0" apply false - id("com.android.library") version "8.13.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.13.2" apply false + id("com.android.library") version "8.13.2" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false } From fb61db10d9fcb59a2f32270b299c6d93e1e8dc90 Mon Sep 17 00:00:00 2001 From: Rosario Fernandes Date: Wed, 17 Dec 2025 16:10:20 +0000 Subject: [PATCH 2/5] Use Gemini 3 Flash preview --- .../com/google/firebase/quickstart/ai/FirebaseAISamples.kt | 3 +-- .../google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index a8df73efc5..c58ae508f6 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt @@ -411,7 +411,7 @@ val FIREBASE_AI_SAMPLES = listOf( ), Sample( title = "SVG Generator", - description = "Use Gemini 2.5 Flash to create SVG illustrations", + description = "Use Gemini 3 Flash preview to create SVG illustrations", navRoute = "svg", categories = listOf(Category.IMAGE, Category.TEXT), initialPrompt = content { @@ -419,7 +419,6 @@ val FIREBASE_AI_SAMPLES = listOf( "a kitten" ) }, - modelName = "gemini-2.5-flash-preview", generationConfig = generationConfig { thinkingConfig { thinkingBudget = -1 diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt index 5411e5a1b1..5d314bb92d 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt @@ -45,7 +45,7 @@ class SvgViewModel( generativeModel = Firebase.ai( backend = sample.backend ).generativeModel( - modelName = sample.modelName ?: "gemini-2.5-flash", + modelName = sample.modelName ?: "gemini-3-flash-preview", systemInstruction = sample.systemInstructions, generationConfig = sample.generationConfig, tools = sample.tools From 60d162f39373701d2615a2997588da9a7985d5ec Mon Sep 17 00:00:00 2001 From: Rosario Fernandes Date: Wed, 17 Dec 2025 16:11:31 +0000 Subject: [PATCH 3/5] undo ChatViewModel changes --- .../firebase/quickstart/ai/feature/text/ChatViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt index f70a0d7f41..33afaad2ee 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt @@ -72,10 +72,9 @@ class ChatViewModel( init { val generativeModel = Firebase.ai( - backend = GenerativeBackend.vertexAI("global") // GenerativeBackend.googleAI() by default + backend = sample.backend // GenerativeBackend.googleAI() by default ).generativeModel( -// modelName = sample.modelName ?: "gemini-3-pro-preview-testing-only", - modelName = sample.modelName ?: "gemini-2.5-pro", + modelName = sample.modelName ?: "gemini-2.5-flash", systemInstruction = sample.systemInstructions, generationConfig = sample.generationConfig, tools = sample.tools From 7ab471fe82518632960493c8ed0494441837370e Mon Sep 17 00:00:00 2001 From: Rosario Fernandes Date: Wed, 17 Dec 2025 16:12:14 +0000 Subject: [PATCH 4/5] undo MainActivity change --- .../java/com/google/firebase/quickstart/ai/MainActivity.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index aca0c97430..e3e37064bc 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -49,10 +49,6 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if(ContextCompat.checkSelfPermission(this, - Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } enableEdgeToEdge() catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat) setContent { From 7ae71bb41c064be7b3dab77a7de6a858ae5e22ce Mon Sep 17 00:00:00 2001 From: Rosario Fernandes Date: Wed, 17 Dec 2025 18:47:40 +0000 Subject: [PATCH 5/5] use add(index, element) instead of addFirst(element) --- .../google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt index 5d314bb92d..4ad04b3bdf 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/svg/SvgViewModel.kt @@ -58,7 +58,7 @@ class SvgViewModel( try { val response = generativeModel.generateContent(prompt) response.text?.let { - _generatedSvgList.addFirst(it) + _generatedSvgList.add(0, it) } _errorMessage.value = null } catch (e: Exception) {