diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index b653e9a4df..d402382fb5 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -19,13 +19,14 @@ jobs:
with:
distribution: 'zulu'
java-version: 17
+ - name: Install Node to use the Firebase CLI
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Check Snippets
run: python scripts/checksnippets.py
- # TODO(thatfiredev): remove this once github.com/firebase/quickstart-android/issues/1672 is fixed
- - name: Remove Firebase Data Connect from CI
- run: python scripts/ci_remove_fdc.py
- name: Copy mock google_services.json
run: ./copy_mock_google_services_json.sh
- name: Build with Gradle (Pull Request)
diff --git a/admob/app/build.gradle.kts b/admob/app/build.gradle.kts
index ae2abe3def..1a1c031306 100644
--- a/admob/app/build.gradle.kts
+++ b/admob/app/build.gradle.kts
@@ -1,9 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
}
tasks {
@@ -16,7 +15,7 @@ android {
defaultConfig {
applicationId = "com.google.samples.quickstart.admobexample"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -28,7 +27,7 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
packaging {
@@ -38,9 +37,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
}
@@ -50,23 +46,23 @@ dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
implementation("androidx.appcompat:appcompat:1.7.1")
- implementation("com.google.android.material:material:1.12.0")
+ implementation("com.google.android.material:material:1.13.0")
implementation("androidx.browser:browser:1.5.0")
- implementation("androidx.navigation:navigation-fragment-ktx:2.9.2")
- implementation("androidx.navigation:navigation-ui-ktx:2.9.2")
+ 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:33.16.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.
implementation("com.google.firebase:firebase-analytics")
- debugImplementation("androidx.fragment:fragment-testing:1.8.8")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ debugImplementation("androidx.fragment:fragment-testing:1.8.9")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
diff --git a/admob/build.gradle.kts b/admob/build.gradle.kts
index c0926a87e2..09575b4178 100644
--- a/admob/build.gradle.kts
+++ b/admob/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
}
allprojects {
diff --git a/admob/gradle/wrapper/gradle-wrapper.properties b/admob/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/admob/gradle/wrapper/gradle-wrapper.properties
+++ b/admob/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/analytics/app/build.gradle.kts b/analytics/app/build.gradle.kts
index 6363217ea6..a393f2a39e 100644
--- a/analytics/app/build.gradle.kts
+++ b/analytics/app/build.gradle.kts
@@ -1,9 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
}
tasks {
@@ -16,7 +15,7 @@ android {
defaultConfig {
applicationId = "com.google.firebase.quickstart.analytics"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -27,7 +26,7 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
@@ -36,10 +35,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
-
buildFeatures {
viewBinding = true
}
@@ -49,20 +44,20 @@ dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
- implementation("com.google.android.material:material:1.12.0")
+ implementation("com.google.android.material:material:1.13.0")
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.2")
+ 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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Analytics
implementation("com.google.firebase:firebase-analytics")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
diff --git a/analytics/build.gradle.kts b/analytics/build.gradle.kts
index a29e9add2d..013f300b72 100644
--- a/analytics/build.gradle.kts
+++ b/analytics/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
}
allprojects {
diff --git a/analytics/gradle/wrapper/gradle-wrapper.properties b/analytics/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/analytics/gradle/wrapper/gradle-wrapper.properties
+++ b/analytics/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/appdistribution/app/build.gradle.kts b/appdistribution/app/build.gradle.kts
index 7e30e7b47e..9a9632e83c 100644
--- a/appdistribution/app/build.gradle.kts
+++ b/appdistribution/app/build.gradle.kts
@@ -1,6 +1,6 @@
+
plugins {
id("com.android.application")
- id("kotlin-android")
id("com.google.gms.google-services")
}
@@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId = "com.google.firebase.appdistributionquickstart"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -22,16 +22,13 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
}
@@ -48,22 +45,21 @@ dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
- implementation("com.google.android.material:material:1.12.0")
+ implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
- 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:33.16.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-beta15")
+ implementation("com.google.firebase:firebase-appdistribution:16.0.0-beta17")
// For an optimal experience using App Distribution, add the Firebase SDK
// for Google Analytics. This is recommended, but not required.
implementation("com.google.firebase:firebase-analytics")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
}
diff --git a/appdistribution/app/src/main/AndroidManifest.xml b/appdistribution/app/src/main/AndroidManifest.xml
index a29b19adf7..a5455f3394 100644
--- a/appdistribution/app/src/main/AndroidManifest.xml
+++ b/appdistribution/app/src/main/AndroidManifest.xml
@@ -2,7 +2,6 @@
("ktlintCheck") {
- val outputDir = "${project.buildDir}/reports/ktlint/"
+ val outputDir = "${project.layout.buildDirectory}/reports/ktlint/"
val inputFiles = project.fileTree("src").include("**/*.kt")
val outputFile = "${outputDir}ktlint-checkstyle-report.xml"
@@ -49,7 +48,8 @@ tasks.register("ktlintCheck") {
"--code-style=android_studio",
"--reporter=plain",
"--reporter=checkstyle,output=${outputFile}",
- "**/*.kt"
+ "**/*.kt",
+ "!**/build/**"
)
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
@@ -68,10 +68,10 @@ fun isNonStable(candidate: ModuleComponentIdentifier): Boolean {
fun isBlockListed(candidate: ModuleComponentIdentifier): Boolean {
return listOf(
"androidx.browser:browser",
+ "androidx.webkit:webkit",
"com.facebook.android",
"com.google.guava",
- "com.github.bumptech.glide",
- "com.google.android.gms"
+ "com.github.bumptech.glide"
).any { keyword ->
keyword in candidate.toString().lowercase()
}
@@ -85,6 +85,6 @@ tasks.withType {
tasks {
register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
}
diff --git a/config/app/build.gradle.kts b/config/app/build.gradle.kts
index ae548e4656..a2a492fe42 100644
--- a/config/app/build.gradle.kts
+++ b/config/app/build.gradle.kts
@@ -1,9 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
}
tasks {
@@ -16,7 +15,7 @@ android {
defaultConfig {
applicationId = "com.google.samples.quickstart.config"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -27,16 +26,13 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -47,10 +43,10 @@ dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
- implementation("com.google.android.material:material:1.12.0")
+ 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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Remote Config
implementation("com.google.firebase:firebase-config")
@@ -59,8 +55,8 @@ dependencies {
// for Google Analytics. This is recommended, but not required.
implementation("com.google.firebase:firebase-analytics")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
diff --git a/config/build.gradle.kts b/config/build.gradle.kts
index c0926a87e2..09575b4178 100644
--- a/config/build.gradle.kts
+++ b/config/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
}
allprojects {
diff --git a/config/gradle/wrapper/gradle-wrapper.properties b/config/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/config/gradle/wrapper/gradle-wrapper.properties
+++ b/config/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/copy_mock_google_services_json.sh b/copy_mock_google_services_json.sh
index 51f1bcabe2..54e013b8a4 100755
--- a/copy_mock_google_services_json.sh
+++ b/copy_mock_google_services_json.sh
@@ -13,7 +13,6 @@ cp mock-google-services.json config/app/google-services.json
cp mock-google-services.json crash/app/google-services.json
cp mock-google-services.json database/app/google-services.json
cp mock-google-services.json dataconnect/app/google-services.json
-cp mock-google-services.json dynamiclinks/app/google-services.json
cp mock-google-services.json firebase-ai/app/google-services.json
cp mock-google-services.json firestore/app/google-services.json
cp mock-google-services.json functions/app/google-services.json
diff --git a/crash/app/build.gradle.kts b/crash/app/build.gradle.kts
index 0c9122af80..920dc73dcd 100644
--- a/crash/app/build.gradle.kts
+++ b/crash/app/build.gradle.kts
@@ -1,10 +1,9 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
- id("com.google.firebase.crashlytics")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
+ alias(libs.plugins.firebase.crashlytics)
}
tasks {
@@ -17,7 +16,7 @@ android {
defaultConfig {
applicationId = "com.google.samples.quickstart.crash"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -28,21 +27,18 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
getByName("debug") {
isMinifyEnabled = false
- testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "test-proguard-rules.pro")
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "test-proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -52,11 +48,11 @@ android {
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
- implementation("com.google.android.material:material:1.12.0")
- implementation("androidx.activity:activity-ktx:1.10.1")
+ implementation("com.google.android.material:material:1.13.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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Crashlytics
implementation("com.google.firebase:firebase-crashlytics")
@@ -69,8 +65,8 @@ dependencies {
implementation("com.google.android.gms:play-services-base:18.5.0")
testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
diff --git a/crash/build.gradle.kts b/crash/build.gradle.kts
index 280a85f351..40b3246d65 100644
--- a/crash/build.gradle.kts
+++ b/crash/build.gradle.kts
@@ -1,11 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
- id("com.google.firebase.crashlytics") version "3.0.4" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
}
allprojects {
@@ -18,6 +17,6 @@ allprojects {
tasks {
register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
}
diff --git a/crash/gradle/wrapper/gradle-wrapper.properties b/crash/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/crash/gradle/wrapper/gradle-wrapper.properties
+++ b/crash/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/database/app/build.gradle.kts b/database/app/build.gradle.kts
index c5cbf3ac0c..bd877f9edb 100644
--- a/database/app/build.gradle.kts
+++ b/database/app/build.gradle.kts
@@ -1,9 +1,8 @@
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
}
tasks {
@@ -27,7 +26,7 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
@@ -35,9 +34,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -50,12 +46,12 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
- implementation("com.google.android.material:material:1.12.0")
- implementation("androidx.navigation:navigation-fragment-ktx:2.9.2")
- implementation("androidx.navigation:navigation-ui-ktx:2.9.2")
+ implementation("com.google.android.material:material:1.13.0")
+ 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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Realtime Database
implementation("com.google.firebase:firebase-database")
@@ -63,13 +59,13 @@ 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")
testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
}
diff --git a/database/build.gradle.kts b/database/build.gradle.kts
index e1a62482c1..81a82fd0d4 100644
--- a/database/build.gradle.kts
+++ b/database/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
}
allprojects {
@@ -17,6 +16,6 @@ allprojects {
tasks {
register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
}
diff --git a/database/gradle/wrapper/gradle-wrapper.properties b/database/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/database/gradle/wrapper/gradle-wrapper.properties
+++ b/database/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/dataconnect/app/build.gradle.kts b/dataconnect/app/build.gradle.kts
index 67dde06181..6c32450d26 100644
--- a/dataconnect/app/build.gradle.kts
+++ b/dataconnect/app/build.gradle.kts
@@ -1,6 +1,5 @@
plugins {
alias(libs.plugins.android.application)
- alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.google.services)
alias(libs.plugins.compose.compiler)
@@ -36,9 +35,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
- kotlinOptions {
- jvmTarget = "1.8"
- }
buildFeatures {
compose = true
}
@@ -51,7 +47,7 @@ android {
}
}
sourceSets.getByName("main") {
- java.srcDirs("build/generated/sources")
+ kotlin.directories.add("build/generated/sources")
}
}
@@ -66,6 +62,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
+ implementation(libs.compose.material.icons)
implementation(libs.compose.navigation)
implementation(libs.androidx.lifecycle.runtime.compose.android)
implementation(libs.coil.compose)
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
index e1a9baa90e..8164f69675 100644
--- a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
@@ -25,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
-import coil.compose.AsyncImage
+import coil3.compose.AsyncImage
import com.google.firebase.dataconnect.movies.GetActorByIdQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
index 148293f348..cc56757bfe 100644
--- a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
@@ -38,7 +38,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
-import coil.compose.AsyncImage
+import coil3.compose.AsyncImage
import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.Actor
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
index 9e783df8df..b60b271c2c 100644
--- a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
@@ -22,7 +22,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
+import coil3.compose.AsyncImage
val ACTOR_CARD_SIZE = 64.dp
diff --git a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
index 0be44d7abd..620910ab47 100644
--- a/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
+++ b/dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
@@ -17,7 +17,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
+import coil3.compose.AsyncImage
/**
* Used to represent a movie in a list UI
diff --git a/dataconnect/build.gradle.kts b/dataconnect/build.gradle.kts
index 8e1379dd6a..11467f73d9 100644
--- a/dataconnect/build.gradle.kts
+++ b/dataconnect/build.gradle.kts
@@ -1,7 +1,23 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
- alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.compose.compiler) apply false
}
+
+tasks {
+ register("dataconnectCompile") {
+ workingDir = project.file("./dataconnect")
+ if (org.apache.tools.ant.taskdefs.condition.Os.isFamily(org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)) {
+ commandLine("npx.cmd", "-y", "firebase-tools@latest", "dataconnect:compile")
+ } else {
+ commandLine("npx", "-y", "firebase-tools@latest", "dataconnect:compile")
+ }
+ isIgnoreExitValue = true
+ }
+
+ register("clean", Delete::class) {
+ delete(rootProject.layout.buildDirectory)
+ finalizedBy("dataconnectCompile")
+ }
+}
\ No newline at end of file
diff --git a/dataconnect/dataconnect/movie-connector/connector.yaml b/dataconnect/dataconnect/movie-connector/connector.yaml
index 2ba51749dc..21099bb852 100644
--- a/dataconnect/dataconnect/movie-connector/connector.yaml
+++ b/dataconnect/dataconnect/movie-connector/connector.yaml
@@ -10,4 +10,4 @@ generate:
package: com.google.firebase.dataconnect.movies
# Specify where to store the generated SDK
# We're using the build/ directory so that generated code doesn't get checked into git
- outputDir: ../../app/build/generated/sources/com/google/firebase/dataconnect/movies
+ outputDir: ../../app/build/generated/sources
diff --git a/dataconnect/dataconnect/movie-connector/queries.gql b/dataconnect/dataconnect/movie-connector/queries.gql
index 5e05b17e22..4e0229e98a 100644
--- a/dataconnect/dataconnect/movie-connector/queries.gql
+++ b/dataconnect/dataconnect/movie-connector/queries.gql
@@ -1,5 +1,5 @@
# List subset of fields for movies
-query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC) {
+query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC, insecureReason: "Test Mode") {
movies(
orderBy: [
{ rating: $orderByRating },
@@ -19,7 +19,7 @@ query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirec
}
# Get movie by id
-query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
+query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Test Mode") {
movie(id: $id) {
id
title
@@ -58,7 +58,7 @@ query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
}
# Get actor by id
-query GetActorById($id: UUID!) @auth(level: PUBLIC) {
+query GetActorById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Test Mode") {
actor(id: $id) {
id
name
@@ -130,7 +130,7 @@ query SearchAll(
$minRating: Float!
$maxRating: Float!
$genre: String!
-) @auth(level: PUBLIC) {
+) @auth(level: PUBLIC, insecureReason: "Test Mode") {
moviesMatchingTitle: movies(
where: {
_and: [
diff --git a/dataconnect/gradle/wrapper/gradle-wrapper.properties b/dataconnect/gradle/wrapper/gradle-wrapper.properties
index 34ddbf468f..2e00545d63 100644
--- a/dataconnect/gradle/wrapper/gradle-wrapper.properties
+++ b/dataconnect/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed May 08 19:29:05 BST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/dynamiclinks/.gitignore b/dynamiclinks/.gitignore
deleted file mode 100644
index d46cccfd2b..0000000000
--- a/dynamiclinks/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-.gradle
-/local.properties
-.DS_Store
-build/
-google-services.json
-
-# Android Studio
-.idea
-*.iml
diff --git a/dynamiclinks/README.md b/dynamiclinks/README.md
index f7223fd50c..ef7cb88371 100644
--- a/dynamiclinks/README.md
+++ b/dynamiclinks/README.md
@@ -6,54 +6,3 @@ Firebase Dynamic Links Quickstart
>
> Please see our [Dynamic Links Deprecation FAQ documentation](https://firebase.google.com/support/dynamic-links-faq) for more guidance.
-Introduction
-------------
-
-- [Read more about Firebase Dynamic Links](https://firebase.google.com/docs/dynamic-links)
-
-Getting Started
----------------
-
-- [Add Firebase to your Android Project](https://firebase.google.com/docs/android/setup).
-- Follow the [quickstart guide](https://firebase.google.com/docs/android/setup) to set up your project.
-- Configure the sample:
- - Replace the `app_code` value in `app/build.gradle` with your personal
- [app code](https://firebase.google.com/docs/dynamic-links/android/create#set-up-firebase-and-the-dynamic-links-sdk).
- - Replace the **applicationId** in `app/build.gradle` with the package name that matches your app code.
-- Run the sample on your Android device or emulator.
-- Using the sample:
- - When the application is started, a deep link will be generated using your app code.
- - Click **Share**
- to share this deep link to another application.
- - The application checks if it was launched from a deep link. If so, the link data will be displayed under the **Receive** heading.
- - Try sharing the deep link from the application and use that deep link to re-launch the application.
-
-Result
------------
-
-
-Support
--------
-
-- [Stack Overflow](https://stackoverflow.com/questions/tagged/firebase-dynamic-links)
-- [Firebase Support](https://firebase.google.com/support/)
-
-License
--------
-
-Copyright 2016 Google, Inc.
-
-Licensed to the Apache Software Foundation (ASF) under one or more contributor
-license agreements. See the NOTICE file distributed with this work for
-additional information regarding copyright ownership. The ASF licenses this
-file to you under the Apache License, Version 2.0 (the "License"); you may not
-use this file except in compliance with the License. You may obtain a copy of
-the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-License for the specific language governing permissions and limitations under
-the License.
diff --git a/dynamiclinks/app/.gitignore b/dynamiclinks/app/.gitignore
deleted file mode 100644
index 796b96d1c4..0000000000
--- a/dynamiclinks/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/dynamiclinks/app/build.gradle.kts b/dynamiclinks/app/build.gradle.kts
deleted file mode 100644
index 75308067f9..0000000000
--- a/dynamiclinks/app/build.gradle.kts
+++ /dev/null
@@ -1,78 +0,0 @@
-import com.android.build.gradle.internal.tasks.factory.dependsOn
-
-plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
-}
-
-tasks {
- check.dependsOn("assembleMainFlavorDebugAndroidTest")
-}
-
-android {
- namespace = "com.google.firebase.quickstart.deeplinks"
- compileSdk = 36
-
- defaultConfig {
- applicationId = "com.google.firebase.quickstart.deeplinks"
- minSdk = 21
- targetSdk = 36
- versionCode = 1
- versionName = "1.0"
- multiDexEnabled = true
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- getByName("release") {
- isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
- }
- }
-
- flavorDimensions += "all"
-
- productFlavors {
- create("mainFlavor") {
- dimension = "all"
-
- // TODO(developer): Replace this with your Dynamic Links URI prefix
- // See: https://firebase.google.com/docs/dynamic-links/android/create#set-up-firebase-and-the-dynamic-links-sdk
- resValue("string", "dynamic_links_uri_prefix", "https://YOUR_APP.page.link")
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
-
- buildFeatures {
- viewBinding = true
- }
-}
-
-dependencies {
- implementation(project(":internal:lintchecks"))
- implementation(project(":internal:chooserx"))
-
- implementation("com.google.android.material:material:1.12.0")
-
- // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
- implementation(platform("com.google.firebase:firebase-bom:33.16.0"))
-
- // Firebase Dynamic Links
- implementation("com.google.firebase:firebase-dynamic-links")
-
- // For an optimal experience using Dynamic Links, add the Firebase SDK
- // for Google Analytics. This is recommended, but not required.
- implementation("com.google.firebase:firebase-analytics")
-
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
-}
diff --git a/dynamiclinks/app/proguard-rules.pro b/dynamiclinks/app/proguard-rules.pro
deleted file mode 100644
index 58708ab1fa..0000000000
--- a/dynamiclinks/app/proguard-rules.pro
+++ /dev/null
@@ -1,20 +0,0 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.kts.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
--keepattributes EnclosingMethod
--keepattributes InnerClasses
diff --git a/dynamiclinks/app/src/androidTest/java/com/google/firebase/quickstart/deeplinks/MainActivityTest.java b/dynamiclinks/app/src/androidTest/java/com/google/firebase/quickstart/deeplinks/MainActivityTest.java
deleted file mode 100644
index 7304ddd76f..0000000000
--- a/dynamiclinks/app/src/androidTest/java/com/google/firebase/quickstart/deeplinks/MainActivityTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.google.firebase.quickstart.deeplinks;
-
-
-import android.content.Intent;
-import android.net.Uri;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.rule.ActivityTestRule;
-
-import com.google.firebase.quickstart.deeplinks.java.MainActivity;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static org.hamcrest.CoreMatchers.allOf;
-import static org.hamcrest.CoreMatchers.containsString;
-
-/**
- * NOTE: This test will fail if R.string.app_code is not set in the build.gradle file
- */
-@RunWith(AndroidJUnit4.class)
-public class MainActivityTest {
-
- @Rule
- public ActivityTestRule rule = new ActivityTestRule<>(
- MainActivity.class);
-
- @Test
- public void testDeepLinkReceive() {
- // Build a deep link
- Uri linkUri = Uri.parse("https://example.com/12345");
- Uri uri = rule.getActivity().buildDeepLink(linkUri, 0);
-
- // Launch an intent to view the deep link
- Intent intent = new Intent()
- .setAction(Intent.ACTION_VIEW)
- .setClass(rule.getActivity().getApplicationContext(), MainActivity.class)
- .setData(uri);
- rule.getActivity().startActivity(intent);
-
- // Confirm the deep link content is displayed
- onView(withId(R.id.linkViewReceive))
- .check(matches(allOf(
- withText(containsString(linkUri.toString())),
- isDisplayed())));
- }
-
-}
diff --git a/dynamiclinks/app/src/main/AndroidManifest.xml b/dynamiclinks/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 615a7f5e9b..0000000000
--- a/dynamiclinks/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/EntryChoiceActivity.kt b/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/EntryChoiceActivity.kt
deleted file mode 100644
index 535651215f..0000000000
--- a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/EntryChoiceActivity.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.google.firebase.quickstart.deeplinks
-
-import android.content.Intent
-import com.firebase.example.internal.BaseEntryChoiceActivity
-import com.firebase.example.internal.Choice
-
-class EntryChoiceActivity : BaseEntryChoiceActivity() {
-
- override fun getChoices(): List {
- return listOf(
- Choice(
- "Java",
- "Run the Firebase Dynamic Links quickstart written in Java.",
- Intent(this, com.google.firebase.quickstart.deeplinks.java.MainActivity::class.java),
- ),
- Choice(
- "Kotlin",
- "Run the Firebase Dynamic Links quickstart written in Kotlin.",
- Intent(this, com.google.firebase.quickstart.deeplinks.kotlin.MainActivity::class.java),
- ),
- )
- }
-}
diff --git a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/java/MainActivity.java b/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/java/MainActivity.java
deleted file mode 100644
index bde97fc687..0000000000
--- a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/java/MainActivity.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/**
- * Copyright 2016 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.firebase.quickstart.deeplinks.java;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.widget.Button;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import android.util.Log;
-import android.view.View;
-import android.widget.TextView;
-
-import com.google.android.gms.tasks.OnFailureListener;
-import com.google.android.gms.tasks.OnSuccessListener;
-import com.google.firebase.dynamiclinks.DynamicLink;
-import com.google.firebase.dynamiclinks.DynamicLink.AndroidParameters;
-import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
-import com.google.firebase.dynamiclinks.PendingDynamicLinkData;
-import com.google.firebase.dynamiclinks.ShortDynamicLink;
-import com.google.firebase.quickstart.deeplinks.R;
-import com.google.firebase.quickstart.deeplinks.databinding.ActivityMainBinding;
-
-public class MainActivity extends AppCompatActivity {
-
- private static final String TAG = "MainActivity";
- private static final String DEEP_LINK_URL = "https://example.com/deeplinks";
-
- // [START on_create]
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // [START_EXCLUDE]
- super.onCreate(savedInstanceState);
- ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- TextView linkSendTextView = binding.linkViewSend;
- final TextView linkReceiveTextView = binding.linkViewReceive;
-
- // Validate that the developer has set the app code.
- validateAppCode();
-
- // Create a deep link and display it in the UI
- final Uri deepLink = buildDeepLink(Uri.parse(DEEP_LINK_URL), 0);
- linkSendTextView.setText(deepLink.toString());
-
- // Share button click listener
- binding.buttonShare.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- shareDeepLink(deepLink.toString());
- }
- });
-
- // create a short link and display it in the UI
- binding.buttonShareShortLink.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- TextView shortLinkTextView = findViewById(R.id.shortLinkViewSend);
- String shortDynamicLink = shortLinkTextView.getText().toString();
- shareDeepLink(shortDynamicLink);
- }
- });
-
- binding.buttonGenerateShortLink.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Uri deepLink = Uri.parse(DEEP_LINK_URL);
- buildShortLinkFromParams(deepLink, 0);
- }
- });
- // [END_EXCLUDE]
-
- // [START get_deep_link]
- FirebaseDynamicLinks.getInstance()
- .getDynamicLink(getIntent())
- .addOnSuccessListener(this, new OnSuccessListener() {
- @Override
- public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
- // Get deep link from result (may be null if no link is found)
- Uri deepLink = null;
- if (pendingDynamicLinkData != null) {
- deepLink = pendingDynamicLinkData.getLink();
- }
-
-
- // Handle the deep link. For example, open the linked
- // content, or apply promotional credit to the user's
- // account.
- // ...
-
- // [START_EXCLUDE]
- // Display deep link in the UI
- if (deepLink != null) {
- Snackbar.make(findViewById(android.R.id.content),
- "Found deep link!", Snackbar.LENGTH_LONG).show();
-
- linkReceiveTextView.setText(deepLink.toString());
- } else {
- Log.d(TAG, "getDynamicLink: no link found");
- }
- // [END_EXCLUDE]
- }
- })
- .addOnFailureListener(this, new OnFailureListener() {
- @Override
- public void onFailure(@NonNull Exception e) {
- Log.w(TAG, "getDynamicLink:onFailure", e);
- }
- });
- // [END get_deep_link]
- }
- // [END on_create]
-
- /**
- * Build a Firebase Dynamic Link.
- * https://firebase.google.com/docs/dynamic-links/android/create#create-a-dynamic-link-from-parameters
- *
- * @param deepLink the deep link your app will open. This link must be a valid URL and use the
- * HTTP or HTTPS scheme.
- * @param minVersion the {@code versionCode} of the minimum version of your app that can open
- * the deep link. If the installed app is an older version, the user is taken
- * to the Play store to upgrade the app. Pass 0 if you do not
- * require a minimum version.
- * @return a {@link Uri} representing a properly formed deep link.
- */
- @VisibleForTesting
- public Uri buildDeepLink(@NonNull Uri deepLink, int minVersion) {
- String uriPrefix = getString(R.string.dynamic_links_uri_prefix);
-
- // Set dynamic link parameters:
- // * URI prefix (required)
- // * Android Parameters (required)
- // * Deep link
- // [START build_dynamic_link]
- DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance()
- .createDynamicLink()
- .setDomainUriPrefix(uriPrefix)
- .setAndroidParameters(new DynamicLink.AndroidParameters.Builder()
- .setMinimumVersion(minVersion)
- .build())
- .setLink(deepLink);
-
- // Build the dynamic link
- DynamicLink link = builder.buildDynamicLink();
- // [END build_dynamic_link]
-
- // Return the dynamic link as a URI
- return link.getUri();
- }
-
- private void shareDeepLink(String deepLink) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_SUBJECT, "Firebase Deep Link");
- intent.putExtra(Intent.EXTRA_TEXT,deepLink);
-
- startActivity(intent);
- }
-
- private void validateAppCode() {
- String uriPrefix = getString(R.string.dynamic_links_uri_prefix);
- if (uriPrefix.contains("YOUR_APP")) {
- new AlertDialog.Builder(this)
- .setTitle("Invalid Configuration")
- .setMessage("Please set your Dynamic Links domain in app/build.gradle")
- .setPositiveButton(android.R.string.ok, null)
- .create().show();
- }
- }
-
- public void generateShortLink() {
- Uri deepLink = Uri.parse(DEEP_LINK_URL);
- buildShortLinkFromParams(deepLink, 0);
- }
-
- @VisibleForTesting
- public void buildShortLinkFromParams(@NonNull Uri deepLink, int minVersion) {
- String uriPrefix = getString(R.string.dynamic_links_uri_prefix);
-
- // Set dynamic link parameters:
- // * URI prefix (required)
- // * Android Parameters (required)
- // * Deep link
- FirebaseDynamicLinks.getInstance()
- .createDynamicLink()
- .setDomainUriPrefix(uriPrefix)
- .setAndroidParameters(
- new AndroidParameters.Builder()
- .setMinimumVersion(minVersion)
- .build())
- .setLink(deepLink)
- .buildShortDynamicLink()
- .addOnCompleteListener(this, new OnCompleteListener() {
- @Override
- public void onComplete(@NonNull Task task) {
- if (task.isSuccessful()) {
- // Short link created
- Uri shortDynamicLink = task.getResult().getShortLink();
- TextView shortLinkTextView = findViewById(R.id.shortLinkViewSend);
- shortLinkTextView.setText(shortDynamicLink.toString());
- } else {
- // Error
- Log.e(TAG, task.getException().getMessage());
- }
- }
- });
- }
-}
diff --git a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/kotlin/MainActivity.kt b/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/kotlin/MainActivity.kt
deleted file mode 100644
index e3f48ce6e9..0000000000
--- a/dynamiclinks/app/src/main/java/com/google/firebase/quickstart/deeplinks/kotlin/MainActivity.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package com.google.firebase.quickstart.deeplinks.kotlin
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import android.widget.TextView
-import androidx.annotation.VisibleForTesting
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
-import com.google.android.material.snackbar.Snackbar
-import com.google.firebase.dynamiclinks.PendingDynamicLinkData
-import com.google.firebase.dynamiclinks.androidParameters
-import com.google.firebase.dynamiclinks.component1
-import com.google.firebase.dynamiclinks.component2
-import com.google.firebase.dynamiclinks.dynamicLink
-import com.google.firebase.dynamiclinks.dynamicLinks
-import com.google.firebase.dynamiclinks.shortLinkAsync
-import com.google.firebase.Firebase
-import com.google.firebase.quickstart.deeplinks.R
-import com.google.firebase.quickstart.deeplinks.databinding.ActivityMainBinding
-
-class MainActivity : AppCompatActivity() {
- // [START on_create]
- override fun onCreate(savedInstanceState: Bundle?) {
- // [START_EXCLUDE]
- super.onCreate(savedInstanceState)
- val binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- val linkSendTextView = binding.linkViewSend
- val linkReceiveTextView = binding.linkViewReceive
-
- // Validate that the developer has set the app code.
- validateAppCode()
-
- // Create a deep link and display it in the UI
- val newDeepLink = buildDeepLink(Uri.parse(DEEP_LINK_URL), 0)
- linkSendTextView.text = newDeepLink.toString()
-
- // Share button click listener
- binding.buttonShare.setOnClickListener { shareDeepLink(newDeepLink.toString()) }
- // [END_EXCLUDE]
-
- binding.buttonShareShortLink.setOnClickListener {
- val shortLinkTextView = findViewById(R.id.shortLinkViewSend)
- val shortDynamicLink = shortLinkTextView.text
- shareDeepLink(shortDynamicLink.toString())
- }
-
- binding.buttonGenerateShortLink.setOnClickListener {
- val deepLink = Uri.parse(DEEP_LINK_URL)
- buildShortLinkFromParams(deepLink, 0)
- }
-
- // [START get_deep_link]
- Firebase.dynamicLinks
- .getDynamicLink(intent)
- .addOnSuccessListener(this) { pendingDynamicLinkData: PendingDynamicLinkData? ->
- // Get deep link from result (may be null if no link is found)
- var deepLink: Uri? = null
- if (pendingDynamicLinkData != null) {
- deepLink = pendingDynamicLinkData.link
- }
-
- // Handle the deep link. For example, open the linked
- // content, or apply promotional credit to the user's
- // account.
- // ...
-
- // [START_EXCLUDE]
- // Display deep link in the UI
- if (deepLink != null) {
- Snackbar.make(
- findViewById(android.R.id.content),
- "Found deep link!",
- Snackbar.LENGTH_LONG,
- ).show()
-
- linkReceiveTextView.text = deepLink.toString()
- } else {
- Log.d(TAG, "getDynamicLink: no link found")
- }
- // [END_EXCLUDE]
- }
- .addOnFailureListener(this) { e -> Log.w(TAG, "getDynamicLink:onFailure", e) }
- // [END get_deep_link]
- }
- // [END on_create]
-
- /**
- * Build a Firebase Dynamic Link.
- * https://firebase.google.com/docs/dynamic-links/android/create#create-a-dynamic-link-from-parameters
- *
- * @param deepLink the deep link your app will open. This link must be a valid URL and use the
- * HTTP or HTTPS scheme.
- * @param minVersion the `versionCode` of the minimum version of your app that can open
- * the deep link. If the installed app is an older version, the user is taken
- * to the Play store to upgrade the app. Pass 0 if you do not
- * require a minimum version.
- * @return a [Uri] representing a properly formed deep link.
- */
- @VisibleForTesting
- fun buildDeepLink(deepLink: Uri, minVersion: Int): Uri {
- val uriPrefix = getString(R.string.dynamic_links_uri_prefix)
-
- // Set dynamic link parameters:
- // * URI prefix (required)
- // * Android Parameters (required)
- // * Deep link
- // [START build_dynamic_link]
- // Build the dynamic link
- val link = Firebase.dynamicLinks.dynamicLink {
- domainUriPrefix = uriPrefix
- androidParameters {
- minimumVersion = minVersion
- }
- link = deepLink
- }
- // [END build_dynamic_link]
-
- // Return the dynamic link as a URI
- return link.uri
- }
-
- @VisibleForTesting
- fun buildShortLinkFromParams(deepLink: Uri, minVersion: Int) {
- val uriPrefix = getString(R.string.dynamic_links_uri_prefix)
-
- // Set dynamic link parameters:
- // * URI prefix (required)
- // * Android Parameters (required)
- // * Deep link
- Firebase.dynamicLinks.shortLinkAsync {
- link = deepLink
- domainUriPrefix = uriPrefix
- androidParameters {
- minimumVersion = minVersion
- }
- }.addOnSuccessListener { (shortLink, flowchartLink) ->
- val shortLinkTextView = findViewById(R.id.shortLinkViewSend)
- shortLinkTextView.text = shortLink.toString()
- }.addOnFailureListener(this) { e ->
- Log.e(TAG, e.toString())
- }
- }
-
- private fun shareDeepLink(deepLink: String) {
- val intent = Intent(Intent.ACTION_SEND)
- intent.type = "text/plain"
- intent.putExtra(Intent.EXTRA_SUBJECT, "Firebase Deep Link")
- intent.putExtra(Intent.EXTRA_TEXT, deepLink)
-
- startActivity(intent)
- }
-
- private fun validateAppCode() {
- val uriPrefix = getString(R.string.dynamic_links_uri_prefix)
- if (uriPrefix.contains("YOUR_APP")) {
- AlertDialog.Builder(this)
- .setTitle("Invalid Configuration")
- .setMessage("Please set your Dynamic Links domain in app/build.gradle")
- .setPositiveButton(android.R.string.ok, null)
- .create().show()
- }
- }
-
- companion object {
-
- private const val TAG = "MainActivity"
- private const val DEEP_LINK_URL = "https://kotlin.example.com/deeplinks"
- }
-}
diff --git a/dynamiclinks/app/src/main/res/drawable-hdpi/firebase_lockup_400.png b/dynamiclinks/app/src/main/res/drawable-hdpi/firebase_lockup_400.png
deleted file mode 100644
index 2eb21fecd1..0000000000
Binary files a/dynamiclinks/app/src/main/res/drawable-hdpi/firebase_lockup_400.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/drawable-mdpi/firebase_lockup_400.png b/dynamiclinks/app/src/main/res/drawable-mdpi/firebase_lockup_400.png
deleted file mode 100644
index ffed5b1825..0000000000
Binary files a/dynamiclinks/app/src/main/res/drawable-mdpi/firebase_lockup_400.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/drawable-xhdpi/firebase_lockup_400.png b/dynamiclinks/app/src/main/res/drawable-xhdpi/firebase_lockup_400.png
deleted file mode 100644
index 78a4a2780f..0000000000
Binary files a/dynamiclinks/app/src/main/res/drawable-xhdpi/firebase_lockup_400.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/drawable-xxhdpi/firebase_lockup_400.png b/dynamiclinks/app/src/main/res/drawable-xxhdpi/firebase_lockup_400.png
deleted file mode 100644
index 4380835f6a..0000000000
Binary files a/dynamiclinks/app/src/main/res/drawable-xxhdpi/firebase_lockup_400.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/layout/activity_main.xml b/dynamiclinks/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index b5e0094841..0000000000
--- a/dynamiclinks/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/dynamiclinks/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dynamiclinks/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index fbd60f71df..0000000000
Binary files a/dynamiclinks/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dynamiclinks/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index aab6cd03d5..0000000000
Binary files a/dynamiclinks/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dynamiclinks/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 8ffae80a03..0000000000
Binary files a/dynamiclinks/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dynamiclinks/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 97753ecef7..0000000000
Binary files a/dynamiclinks/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dynamiclinks/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 0b14aa3a25..0000000000
Binary files a/dynamiclinks/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/dynamiclinks/app/src/main/res/values-w820dp/dimens.xml b/dynamiclinks/app/src/main/res/values-w820dp/dimens.xml
deleted file mode 100644
index 63fc816444..0000000000
--- a/dynamiclinks/app/src/main/res/values-w820dp/dimens.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- 64dp
-
diff --git a/dynamiclinks/app/src/main/res/values/colors.xml b/dynamiclinks/app/src/main/res/values/colors.xml
deleted file mode 100644
index 446f7da319..0000000000
--- a/dynamiclinks/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- #039BE5
- #0288D1
- #FFA000
-
diff --git a/dynamiclinks/app/src/main/res/values/dimens.xml b/dynamiclinks/app/src/main/res/values/dimens.xml
deleted file mode 100644
index 0e502728c5..0000000000
--- a/dynamiclinks/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- 16dp
- 16dp
-
- 160dp
-
diff --git a/dynamiclinks/app/src/main/res/values/strings.xml b/dynamiclinks/app/src/main/res/values/strings.xml
deleted file mode 100644
index e7eae66ea6..0000000000
--- a/dynamiclinks/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
- Firebase Dynamic Links
-
- Share Dynamic Link
- Share Short Dynamic Link
- Generate Short Dynamic Link
- Dynamic Link
- Short Dynamic Link
- Deeplink Received
- No deep link received.
-
diff --git a/dynamiclinks/app/src/main/res/values/styles.xml b/dynamiclinks/app/src/main/res/values/styles.xml
deleted file mode 100644
index 1c56072bd2..0000000000
--- a/dynamiclinks/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/dynamiclinks/app/src/screen.png b/dynamiclinks/app/src/screen.png
deleted file mode 100644
index a50025f863..0000000000
Binary files a/dynamiclinks/app/src/screen.png and /dev/null differ
diff --git a/dynamiclinks/build.gradle.kts b/dynamiclinks/build.gradle.kts
deleted file mode 100644
index e1a62482c1..0000000000
--- a/dynamiclinks/build.gradle.kts
+++ /dev/null
@@ -1,22 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-plugins {
- id("com.android.application") version "8.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
-}
-
-allprojects {
- repositories {
- mavenLocal()
- google()
- mavenCentral()
- }
-}
-
-tasks {
- register("clean", Delete::class) {
- delete(rootProject.buildDir)
- }
-}
diff --git a/dynamiclinks/gradle.properties b/dynamiclinks/gradle.properties
deleted file mode 100644
index 6dd0218ed7..0000000000
--- a/dynamiclinks/gradle.properties
+++ /dev/null
@@ -1,18 +0,0 @@
-# Project-wide Gradle settings.
-
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
-android.useAndroidX=true
-
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
diff --git a/dynamiclinks/gradle/wrapper/gradle-wrapper.jar b/dynamiclinks/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index c1962a79e2..0000000000
Binary files a/dynamiclinks/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/dynamiclinks/gradle/wrapper/gradle-wrapper.properties b/dynamiclinks/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 3ae1e2f124..0000000000
--- a/dynamiclinks/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,5 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/dynamiclinks/gradlew b/dynamiclinks/gradlew
deleted file mode 100755
index aeb74cbb43..0000000000
--- a/dynamiclinks/gradlew
+++ /dev/null
@@ -1,245 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/dynamiclinks/gradlew.bat b/dynamiclinks/gradlew.bat
deleted file mode 100644
index 6689b85bee..0000000000
--- a/dynamiclinks/gradlew.bat
+++ /dev/null
@@ -1,92 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/dynamiclinks/settings.gradle.kts b/dynamiclinks/settings.gradle.kts
deleted file mode 100644
index 76fe7b94aa..0000000000
--- a/dynamiclinks/settings.gradle.kts
+++ /dev/null
@@ -1,18 +0,0 @@
-pluginManagement {
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-
-include(":app")
-
-// Required so that gradle can resolve these dependencies even when
-// building only a single project.
-include(":internal:lintchecks")
-project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
-include(":internal:lint")
-project(":internal:lint").projectDir = file("../internal/lint")
-include(":internal:chooserx")
-project(":internal:chooserx").projectDir = file("../internal/chooserx")
\ No newline at end of file
diff --git a/firebase-ai/README.md b/firebase-ai/README.md
index f3b76b6a7f..e35339307c 100644
--- a/firebase-ai/README.md
+++ b/firebase-ai/README.md
@@ -2,32 +2,68 @@
This Android sample app demonstrates how to use state-of-the-art
generative AI models (like Gemini) to build AI-powered features and applications.
+
For more information about Firebase AI Logic, visit the [documentation](http://firebase.google.com/docs/ai-logic).
+## Setup & Configuration
+
+### Prerequisites
+* **Google AI (Gemini) API Key**: Most samples work out of the box with the Google AI SDK.
+* **Vertex AI**: Samples marked with *(Vertex AI)* require you to enable the Vertex AI API in your Google Cloud project and have your files in Cloud Storage.
+* **Server Prompt Templates**: These samples require you to set up templates in the [Firebase Console](https://console.firebase.google.com/project/_/ai-logic).
+
## Getting Started
To try out this sample app, you need to use latest stable version of Android Studio.
-However, if you want to latest lint checks and AI productivity features in Android
-Studio use the latest preview version of [Android Studio](https://developer.android.com/studio/preview).
+
+* [Set up your Android app for Firebase][setup-android]
+ * Use the package name `com.google.firebase.quickstart.ai`
+* [Set up Firebase AI Logic][setup-ai-logic]
+* Run the app on an Android device or emulator.
## Features
-There are 2 main files that demonstrate the use of Firebase AI Logic:
-
-- [ChatViewModel.kt](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt)
- which can do things such as:
- - [Generate Text](https://firebase.google.com/docs/ai-logic/generate-text)
- - [Generate structured output (JSON)](https://firebase.google.com/docs/ai-logic/generate-structured-output)
- - [Analyze images](https://firebase.google.com/docs/ai-logic/analyze-images)
- - [Analyze video](https://firebase.google.com/docs/ai-logic/analyze-video)
- - [Analyze audio](https://firebase.google.com/docs/ai-logic/analyze-audio)
- - [Analyze documents (PDFs)](https://firebase.google.com/docs/ai-logic/analyze-documents)
- - [Generate images using Gemini 2.0](https://firebase.google.com/docs/ai-logic/generate-images-imagen)
- - [Function calling](https://firebase.google.com/docs/ai-logic/function-calling)
-- [ImagenViewModel](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt)
- which shows how to [Generate images using Imagen models](https://firebase.google.com/docs/ai-logic/generate-images-imagen)
+You can find the implementation for each feature by clicking on the links below:
+
+### Text / Chat
+- [Travel tips](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt): The user wants the model to help a new traveler with travel tips
+- [Chatbot recommendations for courses](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt): A chatbot suggests courses for a performing arts program.
+- [Weather Chat](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt): Use function calling to get the weather conditions for a specific US city on a specific date.
+- [Grounding with Google Search](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt): Use Grounding with Google Search to get responses based on up-to-date information from the web.
+- [Thinking](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt): Gemini 2.5 Flash with dynamic thinking
+- [Server Prompt Templates - Gemini](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt): Generate an invoice using server prompt templates.
+
+### Image analysis / generation
+- [Imagen 4 - image generation](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt): Generate images using Imagen 4
+- [Imagen 3 - Inpainting (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt): Replace part of an image using Imagen 3
+- [Imagen 3 - Outpainting (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt): Expand an image by drawing in more background
+- [Imagen 3 - Subject Reference (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt): Generate an image using a referenced subject (must be an animal)
+- [Imagen 3 - Style Transfer (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt): Change the art style of a cat picture using a reference
+- [Gemini 2.5 Flash Image (aka nanobanana)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt): Generate and/or edit images using Gemini 2.5 Flash Image aka nanobanana
+- [Server Prompt Template - Imagen](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt): Generate an image using a server prompt template.
+- [SVG Generator](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt): Use Gemini 3 Flash preview to create SVG illustrations
+- [Blog post creator (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt): Create a blog post from an image file stored in Cloud Storage.
+
+### Audio analysis
+- [Audio Summarization](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt): Summarize an audio file
+- [Translation from audio (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt): Translate an audio file stored in Cloud Storage
+
+### Video analysis
+- [Hashtags for a video (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt): Generate hashtags for a video ad stored in Cloud Storage
+- [Summarize video](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt): Summarize a video and extract important dialogue.
+
+### Live API (Real-time bidrectional streaming)
+- [ForecastTalk](app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamAudioViewModel.kt): Use bidirectional streaming to get information about weather conditions
+- [Gemini Live (Video input)](app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt): Use bidirectional streaming to chat with Gemini using your phone's camera
+
+### Document (PDFs) analysis
+- [Document comparison (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt): Compare the contents of 2 documents in Cloud Storage.
+
## All samples
The full list of available samples can be found in the
-[FirebaseAISamples.kt file](app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt).
+[FirebaseAISamples.kt file](app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt).
+
+[setup-android]: https://firebase.google.com/docs/android/setup
+[setup-ai-logic]: https://firebase.google.com/docs/ai-logic/get-started?api=dev#set-up-firebase
\ No newline at end of file
diff --git a/firebase-ai/app/build.gradle.kts b/firebase-ai/app/build.gradle.kts
index 7c361bc007..6b47e50869 100644
--- a/firebase-ai/app/build.gradle.kts
+++ b/firebase-ai/app/build.gradle.kts
@@ -1,9 +1,9 @@
+
plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- id("org.jetbrains.kotlin.plugin.compose")
- id("org.jetbrains.kotlin.plugin.serialization")
- id("com.google.gms.google-services")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.google.services)
}
android {
@@ -12,7 +12,7 @@ android {
defaultConfig {
applicationId = "com.google.firebase.quickstart.ai"
- minSdk = 23
+ minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -33,9 +33,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
- kotlinOptions {
- jvmTarget = "11"
- }
buildFeatures {
compose = true
}
@@ -60,10 +57,28 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.kotlinx.serialization.json)
+ // 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)
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)
+ implementation(libs.firebase.ai.ondevice)
+
+ // Image loading
+ implementation(libs.coil.compose)
+ implementation(libs.coil.network.okhttp)
+ implementation(libs.coil.svg)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
@@ -72,4 +87,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
-}
\ No newline at end of file
+}
diff --git a/firebase-ai/app/src/main/AndroidManifest.xml b/firebase-ai/app/src/main/AndroidManifest.xml
index 0ca9215899..bad5b1aa6b 100644
--- a/firebase-ai/app/src/main/AndroidManifest.xml
+++ b/firebase-ai/app/src/main/AndroidManifest.xml
@@ -2,6 +2,15 @@
+
+
+
+
+
+
+
+
+
-
\ 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
deleted file mode 100644
index c61a12aa0a..0000000000
--- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt
+++ /dev/null
@@ -1,244 +0,0 @@
-package com.google.firebase.quickstart.ai
-
-import com.google.firebase.ai.type.FunctionDeclaration
-import com.google.firebase.ai.type.GenerativeBackend
-import com.google.firebase.ai.type.ResponseModality
-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.quickstart.ai.ui.navigation.Category
-import com.google.firebase.quickstart.ai.ui.navigation.Sample
-
-val FIREBASE_AI_SAMPLES = listOf(
- Sample(
- title = "Travel tips",
- description = "The user wants the model to help a new traveler" +
- " with travel tips",
- navRoute = "chat",
- categories = listOf(Category.TEXT),
- systemInstructions = content {
- text(
- "You are a Travel assistant. You will answer" +
- " questions the user asks based on the information listed" +
- " in Relevant Information. Do not hallucinate. Do not use" +
- " the internet."
- )
- },
- chatHistory = listOf(
- content {
- role = "user"
- text("I have never traveled before. When should I book a flight?")
- },
- content {
- role = "model"
- text(
- "You should book flights a couple of months ahead of time." +
- " It will be cheaper and more flexible for you."
- )
- },
- content {
- role = "user"
- text("Do I need a passport?")
- },
- content {
- role = "model"
- text(
- "If you are traveling outside your own country, make sure" +
- " your passport is up-to-date and valid for more" +
- " than 6 months during your travel."
- )
- }
- ),
- initialPrompt = content { text("What else is important when traveling?") }
- ),
- Sample(
- title = "Chatbot recommendations for courses",
- description = "A chatbot suggests courses for a performing arts program.",
- navRoute = "chat",
- categories = listOf(Category.TEXT),
- systemInstructions = content {
- text(
- "You are a chatbot for the county's performing and fine arts" +
- " program. You help students decide what course they will" +
- " take during the summer."
- )
- },
- initialPrompt = content {
- text("I am interested in Performing Arts. I have taken Theater 1A.")
- }
- ),
- Sample(
- title = "Audio Summarization",
- description = "Summarize an audio file",
- navRoute = "chat",
- categories = listOf(Category.AUDIO),
- chatHistory = listOf(
- content { text("Can you help me summarize an audio file?") },
- content("model") {
- text(
- "Of course! Click on the attach button" +
- " below and choose an audio file for me to summarize."
- )
- }
- ),
- initialPrompt = content {
- text(
- "I have attached the audio file. Please analyze it and summarize the contents" +
- " of the audio as bullet points."
- )
- }
- ),
- Sample(
- title = "Translation from audio (Vertex AI)",
- description = "Translate an audio file stored in Cloud Storage",
- navRoute = "chat",
- categories = listOf(Category.AUDIO),
- backend = GenerativeBackend.vertexAI(),
- initialPrompt = content {
- fileData(
- "https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/" +
- "How_to_create_a_My_Map_in_Google_Maps.mp3",
- "audio/mpeg"
- )
- text("Please translate the audio to Mandarin.")
- }
- ),
- Sample(
- title = "Blog post creator (Vertex AI)",
- description = "Create a blog post from an image file stored in Cloud Storage.",
- navRoute = "chat",
- categories = listOf(Category.IMAGE),
- backend = GenerativeBackend.vertexAI(),
- initialPrompt = content {
- fileData(
- "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/meal-prep.jpeg",
- "image/jpeg"
- )
- text(
- "Write a short, engaging blog post based on this picture." +
- " It should include a description of the meal in the" +
- " photo and talk about my journey meal prepping."
- )
- }
- ),
- Sample(
- title = "Imagen 3 - image generation",
- description = "Generate images using Imagen 3",
- navRoute = "imagen",
- categories = listOf(Category.IMAGE),
- initialPrompt = content {
- text(
- "A photo of a modern building with water in the background"
- )
- }
- ),
- Sample(
- title = "Gemini 2.0 Flash - image generation",
- description = "Generate and/or edit images using Gemini 2.0 Flash",
- navRoute = "chat",
- categories = listOf(Category.IMAGE),
- modelName = "gemini-2.0-flash-preview-image-generation",
- initialPrompt = content {
- text(
- "Hi, can you create a 3d rendered image of a pig " +
- "with wings and a top hat flying over a happy " +
- "futuristic scifi city with lots of greenery?"
- )
- },
- generationConfig = generationConfig {
- responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
- }
- ),
- Sample(
- title = "Document comparison (Vertex AI)",
- description = "Compare the contents of 2 documents." +
- " Only supported by the Vertex AI Gemini API because the documents are stored in Cloud Storage",
- navRoute = "chat",
- categories = listOf(Category.DOCUMENT),
- backend = GenerativeBackend.vertexAI(),
- initialPrompt = content {
- fileData(
- "https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2013.pdf",
- "application/pdf"
- )
- fileData(
- "https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2023.pdf",
- "application/pdf"
- )
- text(
- "The first document is from 2013, and the second document is" +
- " from 2023. How did the standard deduction evolve?"
- )
- }
- ),
- Sample(
- title = "Hashtags for a video (Vertex AI)",
- description = "Generate hashtags for a video ad stored in Cloud Storage",
- navRoute = "chat",
- categories = listOf(Category.VIDEO),
- backend = GenerativeBackend.vertexAI(),
- initialPrompt = content {
- fileData(
- "https://storage.googleapis.com/cloud-samples-data/generative-ai/video/google_home_celebrity_ad.mp4",
- "video/mpeg"
- )
- text(
- "Generate 5-10 hashtags that relate to the video content." +
- " Try to use more popular and engaging terms," +
- " e.g. #Viral. Do not add content not related to" +
- " the video.\n Start the output with 'Tags:'"
- )
- }
- ),
- Sample(
- title = "Summarize video",
- description = "Summarize a video and extract important dialogue.",
- navRoute = "chat",
- categories = listOf(Category.VIDEO),
- chatHistory = listOf(
- content { text("Can you help me with the description of a video file?") },
- content("model") {
- text(
- "Sure! Click on the attach button below and choose a" +
- " video file for me to describe."
- )
- }
- ),
- initialPrompt = content {
- text(
- "I have attached the video file. Provide a description of" +
- " the video. The description should also contain" +
- " anything important which people say in the video."
- )
- }
- ),
- Sample(
- title = "Weather Chat",
- description = "Use function calling to get the weather conditions" +
- " for a specific US city on a specific date.",
- navRoute = "chat",
- categories = listOf(Category.TEXT, 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?")
- }
- ),
-)
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 998e4612a7..51ed6ce398 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,5 +1,8 @@
package com.google.firebase.quickstart.ai
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -17,18 +20,28 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-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.media.imagen.ImagenRoute
-import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenScreen
-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.live.BidiViewModel
+import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel
+import com.google.firebase.quickstart.ai.feature.text.ChatViewModel
+import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
+import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
+import com.google.firebase.quickstart.ai.ui.ChatScreen
+import com.google.firebase.quickstart.ai.ui.ImagenScreen
+import com.google.firebase.quickstart.ai.ui.ServerPromptScreen
+import com.google.firebase.quickstart.ai.ui.StreamRealtimeScreen
+import com.google.firebase.quickstart.ai.ui.StreamRealtimeVideoScreen
+import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen
+import com.google.firebase.quickstart.ai.ui.SvgScreen
+import com.google.firebase.quickstart.ai.ui.navigation.FIREBASE_AI_SAMPLES
import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen
+import com.google.firebase.quickstart.ai.ui.navigation.ScreenType
import com.google.firebase.quickstart.ai.ui.theme.FirebaseAILogicTheme
class MainActivity : ComponentActivity() {
@@ -36,6 +49,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat)
setContent {
val navController = rememberNavController()
@@ -46,7 +60,7 @@ class MainActivity : ComponentActivity() {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
- titleContentColor = MaterialTheme.colorScheme.primary
+ titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
title = {
Text(topBarTitle)
@@ -66,48 +80,70 @@ class MainActivity : ComponentActivity() {
MainMenuScreen(
onSampleClicked = {
topBarTitle = it.title
- when (it.navRoute) {
- "chat" -> {
- navController.navigate(ChatRoute(it.id))
- }
+ navController.navigate(it.route)
+ }
+ )
+ }
+
+ // Add navigation for all of the samples
+ FIREBASE_AI_SAMPLES.forEach { sample ->
+ composable(
+ route = sample.route::class,
+ typeMap = emptyMap()
+ ) {
+ val viewModelClass = sample.viewModelClass?.java
+ ?: return@composable
+ val vm = viewModel(modelClass = viewModelClass)
+
+ when (sample.screenType) {
+ ScreenType.CHAT -> {
+ (vm as? ChatViewModel)?.let { ChatScreen(it) }
+ }
+
+ ScreenType.IMAGEN -> {
+ (vm as? ImagenViewModel)?.let { ImagenScreen(it) }
+ }
+
+ ScreenType.SVG -> {
+ (vm as? SvgViewModel)?.let { SvgScreen(it) }
+ }
+
+ ScreenType.SERVER_PROMPT -> {
+ (vm as? ServerPromptTemplateViewModel)?.let { ServerPromptScreen(it) }
+ }
- "imagen" -> {
- navController.navigate(ImagenRoute(it.id))
+ ScreenType.BIDI -> {
+ (vm as? BidiViewModel)?.let {
+ @SuppressLint("MissingPermission")
+ StreamRealtimeScreen(it)
}
+ }
- "stream" -> {
- navController.navigate(StreamRealtimeRoute(it.id))
+ ScreenType.BIDI_VIDEO -> {
+ (vm as? BidiViewModel)?.let {
+ @SuppressLint("MissingPermission")
+ StreamRealtimeVideoScreen(it)
}
}
+
+ ScreenType.HYBRID -> {
+ (vm as? HybridInferenceViewModel)?.let { HybridInferenceScreen(it) }
+ }
}
- )
- }
- // Text Samples
- composable {
- ChatScreen()
- }
- // Imagen Samples
- composable {
- ImagenScreen()
- }
- // Stream Realtime Samples
- composable {
- StreamRealtimeScreen()
+ }
}
}
}
}
- navController.addOnDestinationChangedListener(object : NavController.OnDestinationChangedListener {
- override fun onDestinationChanged(
- controller: NavController,
- destination: NavDestination,
- arguments: Bundle?
- ) {
- if (destination.route == "mainMenu") {
- topBarTitle = getString(R.string.app_name)
- }
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ if (destination.route == "mainMenu") {
+ topBarTitle = getString(R.string.app_name)
}
- })
+ }
}
}
+
+ companion object {
+ lateinit var catImage: Bitmap
+ }
}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/Expense.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/Expense.kt
new file mode 100644
index 0000000000..60bf5cf323
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/Expense.kt
@@ -0,0 +1,10 @@
+package com.google.firebase.quickstart.ai.feature.hybrid
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Expense(
+ val name: String,
+ val price: Double,
+ val inferenceMode: String = ""
+)
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/HybridInferenceViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/HybridInferenceViewModel.kt
new file mode 100644
index 0000000000..d0644026cf
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/HybridInferenceViewModel.kt
@@ -0,0 +1,153 @@
+package com.google.firebase.quickstart.ai.feature.hybrid
+
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.Firebase
+import com.google.firebase.ai.InferenceMode
+import com.google.firebase.ai.InferenceSource
+import com.google.firebase.ai.OnDeviceConfig
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.ondevice.DownloadStatus
+import com.google.firebase.ai.ondevice.FirebaseAIOnDevice
+import com.google.firebase.ai.ondevice.OnDeviceModelStatus
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.HybridInferenceUiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.util.UUID
+
+@Serializable
+object HybridInferenceRoute
+
+@OptIn(PublicPreviewAPI::class)
+class HybridInferenceViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(
+ HybridInferenceUiState(
+ expenses = listOf(
+ Expense("Lunch", 15.50, "Example data"),
+ Expense("Coffee", 4.75, "Example data")
+ )
+ )
+ )
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(
+ modelName = "gemini-3.1-flash-lite-preview",
+ onDeviceConfig = OnDeviceConfig(mode = InferenceMode.PREFER_ON_DEVICE)
+ )
+
+ init {
+ checkAndDownloadModel()
+ }
+
+ private fun checkAndDownloadModel() {
+ viewModelScope.launch {
+ try {
+ val status = FirebaseAIOnDevice.checkStatus()
+ updateStatus(status)
+
+ if (status == OnDeviceModelStatus.DOWNLOADABLE) {
+ FirebaseAIOnDevice.download().collect { downloadStatus ->
+ when (downloadStatus) {
+ is DownloadStatus.DownloadStarted -> {
+ _uiState.update { it.copy(modelStatus = "Downloading model...") }
+ }
+
+ is DownloadStatus.DownloadInProgress -> {
+ val progress = downloadStatus.totalBytesDownloaded
+ _uiState.update { it.copy(modelStatus = "Downloading: $progress bytes downloaded") }
+ }
+
+ is DownloadStatus.DownloadCompleted -> {
+ _uiState.update { it.copy(modelStatus = "Model ready") }
+ }
+
+ is DownloadStatus.DownloadFailed -> {
+ _uiState.update {
+ it.copy(
+ modelStatus = "Download failed", errorMessage = "Model download failed"
+ )
+ }
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ _uiState.update { it.copy(modelStatus = "Error checking status", errorMessage = e.message) }
+ }
+ }
+ }
+
+ private fun updateStatus(status: OnDeviceModelStatus) {
+ val statusText = when (status) {
+ OnDeviceModelStatus.AVAILABLE -> "Model available"
+ OnDeviceModelStatus.DOWNLOADABLE -> "Model downloadable"
+ OnDeviceModelStatus.DOWNLOADING -> "Model downloading..."
+ OnDeviceModelStatus.UNAVAILABLE -> "On-device model unavailable"
+ else -> "Unknown"
+ }
+ _uiState.update { it.copy(modelStatus = statusText) }
+ }
+
+ fun scanReceipt(bitmap: Bitmap) {
+ viewModelScope.launch {
+ _uiState.update { it.copy(isScanning = true, errorMessage = null) }
+ try {
+ val prompt = content {
+ image(bitmap)
+ text(
+ """
+ Extract the store name and the total price from this receipt.
+ Output only in JSON format containg 2 fields '{name,price}'.
+ Do not include any currency signs or backticks or any text around it.
+ Use dots for decimals.
+ Examples:
+ - {"name": "FakeStore", "price": "2.0"}
+ - {"name": "SomeMarket", "price": "3.5"}
+ """.trimIndent()
+ )
+ }
+
+ val response = model.generateContent(prompt)
+ val text = response.text
+ val inferenceMode = if (response.inferenceSource == InferenceSource.ON_DEVICE) {
+ "On-device"
+ } else {
+ "Cloud"
+ }
+ Log.d("HybridVM", "$inferenceMode response: $text")
+ if (text != null) {
+ parseAndAddExpense(text, inferenceMode)
+ } else {
+ _uiState.update { it.copy(errorMessage = "Could not extract data") }
+ }
+ } catch (e: Exception) {
+ _uiState.update { it.copy(errorMessage = "Error: ${e.message}") }
+ } finally {
+ _uiState.update { it.copy(isScanning = false) }
+ }
+ }
+ }
+
+ private fun parseAndAddExpense(text: String, inferenceMode: String) {
+ val json = text
+ // The on-device model sometimes outputs backticks, so we remove those
+ .replace("```json", "")
+ .replace("```", "")
+ try {
+ val newExpense = Json.decodeFromString(json).copy(inferenceMode = inferenceMode)
+ _uiState.update { it.copy(expenses = it.expenses + newExpense) }
+ } catch (e: Exception) {
+ _uiState.update { it.copy(errorMessage = e.localizedMessage) }
+ }
+ }
+}
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
new file mode 100644
index 0000000000..547931580c
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt
@@ -0,0 +1,45 @@
+package com.google.firebase.quickstart.ai.feature.live
+
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.ai.type.FunctionCallPart
+import com.google.firebase.ai.type.FunctionResponsePart
+import com.google.firebase.ai.type.InlineData
+import com.google.firebase.ai.type.LiveSession
+import com.google.firebase.ai.type.PublicPreviewAPI
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonObject
+import java.io.ByteArrayOutputStream
+
+
+@OptIn(PublicPreviewAPI::class)
+abstract class BidiViewModel : ViewModel() {
+ protected lateinit var liveSession: LiveSession
+
+ open fun handler(functionCall: FunctionCallPart): FunctionResponsePart {
+ return FunctionResponsePart(functionCall.name, JsonObject(emptyMap()), functionCall.id)
+ }
+
+ // The permission check is handled by the view that calls this function.
+ @SuppressLint("MissingPermission")
+ suspend fun startConversation() {
+ 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/StreamAudioViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamAudioViewModel.kt
new file mode 100644
index 0000000000..edea6ddf16
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamAudioViewModel.kt
@@ -0,0 +1,82 @@
+package com.google.firebase.quickstart.ai.feature.live
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.FunctionCallPart
+import com.google.firebase.ai.type.FunctionDeclaration
+import com.google.firebase.ai.type.FunctionResponsePart
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.ResponseModality
+import com.google.firebase.ai.type.Schema
+import com.google.firebase.ai.type.SpeechConfig
+import com.google.firebase.ai.type.Tool
+import com.google.firebase.ai.type.Voice
+import com.google.firebase.ai.type.liveGenerationConfig
+import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+@Serializable
+object StreamRealtimeAudioRoute
+
+@OptIn(PublicPreviewAPI::class)
+class StreamAudioViewModel : BidiViewModel() {
+ init {
+ val liveGenerationConfig = liveGenerationConfig {
+ speechConfig = SpeechConfig(voice = Voice("CHARON"))
+ responseModality = ResponseModality.AUDIO
+ }
+
+ val liveModel =
+ Firebase.ai(backend = GenerativeBackend.googleAI())
+ .liveModel(
+ // Note that each backend supports a different set of models.
+ // See our documentation for a breakdown of models by backend:
+ // https://firebase.google.com/docs/ai-logic/live-api#supported-models
+ modelName = "gemini-2.5-flash-native-audio-preview-09-2025",
+ generationConfig = liveGenerationConfig,
+ 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."
+ ),
+ ),
+ )
+ )
+ )
+ ),
+ )
+ runBlocking { liveSession = liveModel.connect() }
+ }
+
+ override fun handler(functionCall: FunctionCallPart): FunctionResponsePart {
+ val response: JsonObject
+ if (functionCall.name == "fetchWeather") {
+ val city = functionCall.args["city"]?.jsonPrimitive?.content
+ val state = functionCall.args["state"]?.jsonPrimitive?.content
+ val date = functionCall.args["date"]?.jsonPrimitive?.content
+ runBlocking {
+ response =
+ if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and !date.isNullOrEmpty()) {
+ fetchWeather(city!!, state!!, date!!)
+ } else {
+ JsonObject(emptyMap())
+ }
+ }
+ } else {
+ response = JsonObject(emptyMap())
+ }
+ return FunctionResponsePart(functionCall.name, response, functionCall.id)
+ }
+}
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
deleted file mode 100644
index 8cbdd721f6..0000000000
--- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.google.firebase.quickstart.ai.feature.live
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import kotlinx.serialization.Serializable
-
-@Serializable
-class StreamRealtimeRoute(val sampleId: String)
-
-@Composable
-fun StreamRealtimeScreen() {
- Box(
- modifier = Modifier.fillMaxSize()
- ) {
- Text("Coming soon")
- }
-}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt
new file mode 100644
index 0000000000..8e715785cb
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt
@@ -0,0 +1,36 @@
+package com.google.firebase.quickstart.ai.feature.live
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+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.Voice
+import com.google.firebase.ai.type.liveGenerationConfig
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.Serializable
+
+@Serializable
+object StreamRealtimeVideoRoute
+
+@OptIn(PublicPreviewAPI::class)
+class StreamVideoViewModel : BidiViewModel() {
+ init {
+ val liveGenerationConfig = liveGenerationConfig {
+ speechConfig = SpeechConfig(voice = Voice("CHARON"))
+ responseModality = ResponseModality.AUDIO
+ }
+
+ // Note that each backend supports a different set of models.
+ // See our documentation for a breakdown of models by backend:
+ // https://firebase.google.com/docs/ai-logic/live-api#supported-models
+ val liveModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).liveModel(
+ modelName = "gemini-2.5-flash-native-audio-preview-09-2025",
+ generationConfig = liveGenerationConfig,
+ )
+ runBlocking { liveSession = liveModel.connect() }
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt
new file mode 100644
index 0000000000..1bcd6fb867
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt
@@ -0,0 +1,55 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenInlineImage
+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.PublicPreviewAPI
+import com.google.firebase.ai.type.imagenGenerationConfig
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenGenerationRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenGenerationViewModel : ImagenViewModel() {
+ override val initialPrompt: String = ""
+ override val includeAttach: Boolean = false
+ override val selectionOptions: List = emptyList()
+ override val allowEmptyPrompt: Boolean = false
+ override val additionalImage: Bitmap? = null
+ override val imageLabels: List = emptyList()
+
+ private val imagenModel: ImagenModel
+
+ init {
+ imagenModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).imagenModel(
+ modelName = "imagen-4.0-generate-001",
+ generationConfig = imagenGenerationConfig {
+ numberOfImages = 4
+ imageFormat = ImagenImageFormat.png()
+ },
+ safetySettings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ )
+ )
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ return imagenModel.generateImages(inputText)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt
new file mode 100644
index 0000000000..40e249e315
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt
@@ -0,0 +1,72 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenBackgroundMask
+import com.google.firebase.ai.type.ImagenEditMode
+import com.google.firebase.ai.type.ImagenEditingConfig
+import com.google.firebase.ai.type.ImagenForegroundMask
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenInlineImage
+import com.google.firebase.ai.type.ImagenPersonFilterLevel
+import com.google.firebase.ai.type.ImagenRawImage
+import com.google.firebase.ai.type.ImagenSafetyFilterLevel
+import com.google.firebase.ai.type.ImagenSafetySettings
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.imagenGenerationConfig
+import com.google.firebase.ai.type.toImagenInlineImage
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenInpaintingRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenInpaintingViewModel : ImagenViewModel() {
+ override val initialPrompt: String = "A sunny beach"
+ override val includeAttach: Boolean = true
+ override val selectionOptions: List = listOf("Mask", "Background", "Foreground")
+ override val allowEmptyPrompt: Boolean = true
+ override val additionalImage: Bitmap? = null
+ override val imageLabels: List = emptyList()
+
+ private val imagenModel: ImagenModel
+
+ init {
+ val config = imagenGenerationConfig {
+ numberOfImages = 4
+ imageFormat = ImagenImageFormat.png()
+ }
+ val settings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ )
+ imagenModel = Firebase.ai(
+ backend = GenerativeBackend.vertexAI()
+ ).imagenModel(
+ modelName = "imagen-3.0-capability-001",
+ generationConfig = config,
+ safetySettings = settings
+ )
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ val bitmap = currentState.attachedImage!!
+ val mask = when (currentState.selectedOption) {
+ "Foreground" -> ImagenForegroundMask()
+ else -> ImagenBackgroundMask()
+ }
+ return imagenModel.editImage(
+ listOfNotNull(ImagenRawImage(bitmap.toImagenInlineImage()), mask),
+ inputText,
+ ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
+ )
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt
new file mode 100644
index 0000000000..1f85333639
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt
@@ -0,0 +1,82 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Dimensions
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenEditMode
+import com.google.firebase.ai.type.ImagenEditingConfig
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenImagePlacement
+import com.google.firebase.ai.type.ImagenInlineImage
+import com.google.firebase.ai.type.ImagenMaskReference
+import com.google.firebase.ai.type.ImagenPersonFilterLevel
+import com.google.firebase.ai.type.ImagenRawMask
+import com.google.firebase.ai.type.ImagenSafetyFilterLevel
+import com.google.firebase.ai.type.ImagenSafetySettings
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.imagenGenerationConfig
+import com.google.firebase.ai.type.toImagenInlineImage
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenOutpaintingRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenOutpaintingViewModel : ImagenViewModel() {
+ override val initialPrompt: String = ""
+ override val includeAttach: Boolean = true
+ override val selectionOptions: List = listOf("Image Alignment", "Center", "Top", "Bottom", "Left", "Right")
+ override val allowEmptyPrompt: Boolean = true
+ override val additionalImage: Bitmap? = null
+ override val imageLabels: List = emptyList()
+
+ private val imagenModel: ImagenModel
+
+ init {
+ val config = imagenGenerationConfig {
+ numberOfImages = 4
+ imageFormat = ImagenImageFormat.png()
+ }
+ val settings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ )
+ imagenModel = Firebase.ai(
+ backend = GenerativeBackend.vertexAI()
+ ).imagenModel(
+ modelName = "imagen-3.0-capability-001",
+ generationConfig = config,
+ safetySettings = settings
+ )
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ val bitmap = currentState.attachedImage!!
+ val position = when (currentState.selectedOption) {
+ "Top" -> ImagenImagePlacement.TOP_CENTER
+ "Bottom" -> ImagenImagePlacement.BOTTOM_CENTER
+ "Left" -> ImagenImagePlacement.LEFT_CENTER
+ "Right" -> ImagenImagePlacement.RIGHT_CENTER
+ else -> ImagenImagePlacement.CENTER
+ }
+ val dimensions = Dimensions(bitmap.width * 2, bitmap.height * 2)
+ val (sourceImage, mask) = ImagenMaskReference.generateMaskAndPadForOutpainting(
+ bitmap.toImagenInlineImage(),
+ dimensions,
+ position
+ )
+ return imagenModel.editImage(
+ listOf(sourceImage, ImagenRawMask(mask.image!!, 0.05)),
+ inputText,
+ ImagenEditingConfig(ImagenEditMode.OUTPAINT)
+ )
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt
new file mode 100644
index 0000000000..8186811ebe
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt
@@ -0,0 +1,68 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenInlineImage
+import com.google.firebase.ai.type.ImagenPersonFilterLevel
+import com.google.firebase.ai.type.ImagenRawImage
+import com.google.firebase.ai.type.ImagenSafetyFilterLevel
+import com.google.firebase.ai.type.ImagenSafetySettings
+import com.google.firebase.ai.type.ImagenStyleReference
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.imagenGenerationConfig
+import com.google.firebase.ai.type.toImagenInlineImage
+import com.google.firebase.quickstart.ai.MainActivity
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenStyleTransferRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenStyleTransferViewModel : ImagenViewModel() {
+ override val initialPrompt: String = "A picture of a cat"
+ override val includeAttach: Boolean = true
+ override val selectionOptions: List = emptyList()
+ override val allowEmptyPrompt: Boolean = true
+ override val additionalImage: Bitmap = MainActivity.catImage
+ override val imageLabels: List = listOf("Style Target", "Style Source")
+
+ private val imagenModel: ImagenModel
+
+ init {
+ val config = imagenGenerationConfig {
+ numberOfImages = 4
+ imageFormat = ImagenImageFormat.png()
+ }
+ val settings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ )
+ imagenModel = Firebase.ai(
+ backend = GenerativeBackend.vertexAI()
+ ).imagenModel(
+ modelName = "imagen-3.0-capability-001",
+ generationConfig = config,
+ safetySettings = settings
+ )
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ val attachedImage = currentState.attachedImage!!
+ return imagenModel.editImage(
+ listOf(
+ ImagenRawImage(MainActivity.catImage.toImagenInlineImage()),
+ ImagenStyleReference(attachedImage.toImagenInlineImage(), 1, "an art style")
+ ),
+ "Generate an image in an art style [1] based on the following caption: $inputText",
+ )
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt
new file mode 100644
index 0000000000..a0a6ef8f05
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt
@@ -0,0 +1,72 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.ImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenImageFormat
+import com.google.firebase.ai.type.ImagenInlineImage
+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.ImagenSubjectReference
+import com.google.firebase.ai.type.ImagenSubjectReferenceType
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.ai.type.imagenGenerationConfig
+import com.google.firebase.ai.type.toImagenInlineImage
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenSubjectReferenceRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenSubjectReferenceViewModel : ImagenViewModel() {
+ override val initialPrompt: String = " flying through space"
+ override val includeAttach: Boolean = true
+ override val selectionOptions: List = emptyList()
+ override val allowEmptyPrompt: Boolean = false
+ override val additionalImage: Bitmap? = null
+ override val imageLabels: List = emptyList()
+
+ private val imagenModel: ImagenModel
+
+ init {
+ val config = imagenGenerationConfig {
+ numberOfImages = 4
+ imageFormat = ImagenImageFormat.png()
+ }
+ val settings = ImagenSafetySettings(
+ safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
+ personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
+ )
+ imagenModel = Firebase.ai(
+ backend = GenerativeBackend.vertexAI()
+ ).imagenModel(
+ modelName = "imagen-3.0-capability-001",
+ generationConfig = config,
+ safetySettings = settings
+ )
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ val attachedImage = currentState.attachedImage!!
+ return imagenModel.editImage(
+ listOf(
+ ImagenSubjectReference(
+ referenceId = 1,
+ image = attachedImage.toImagenInlineImage(),
+ subjectType = ImagenSubjectReferenceType.ANIMAL,
+ description = "An animal"
+ )
+ ),
+ "Create an image about An animal [1] to match the description: " +
+ inputText.replace("", "An animal [1]"),
+ )
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt
new file mode 100644
index 0000000000..2e882faa1f
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt
@@ -0,0 +1,52 @@
+package com.google.firebase.quickstart.ai.feature.media.imagen
+
+import android.graphics.Bitmap
+import com.google.firebase.Firebase
+import com.google.firebase.ai.TemplateImagenModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenInlineImage
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ImagenTemplateRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ImagenTemplateViewModel : ImagenViewModel() {
+ override val initialPrompt: String = "List of things that should be in the image"
+ override val includeAttach: Boolean = false
+ override val selectionOptions: List = emptyList()
+ override val allowEmptyPrompt: Boolean = false
+ override val additionalImage: Bitmap? = null
+ override val imageLabels: List = emptyList()
+
+ private var templateImagenModel: TemplateImagenModel
+
+ init {
+ templateImagenModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).templateImagenModel()
+ }
+
+ override suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse {
+ return try {
+ templateImagenModel.generateImages("imagen-basic", mapOf("prompt" to inputText))
+ } catch (e: Exception) {
+ if (e.localizedMessage?.contains("not found") == true) {
+ throw Exception(
+ """
+ Template was not found, please verify that your project contains a template named "imagen-basic".
+ """.trimIndent()
+ )
+ } else {
+ throw e
+ }
+ }
+ }
+}
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 bd1b58b018..a10d91e337 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
@@ -1,80 +1,65 @@
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
-import androidx.lifecycle.SavedStateHandle
+import android.graphics.BitmapFactory
+import androidx.core.graphics.scale
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.navigation.toRoute
-import com.google.firebase.Firebase
-import com.google.firebase.ai.ImagenModel
-import com.google.firebase.ai.ai
-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.ImagenGenerationResponse
+import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.PublicPreviewAPI
-import com.google.firebase.ai.type.asTextOrNull
-import com.google.firebase.ai.type.imagenGenerationConfig
-import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
+import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@OptIn(PublicPreviewAPI::class)
-class ImagenViewModel(
- 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()
+abstract class ImagenViewModel : ViewModel() {
- private val _errorMessage: MutableStateFlow = MutableStateFlow(null)
- val errorMessage: StateFlow = _errorMessage
+ abstract val initialPrompt: String
+ abstract val includeAttach: Boolean
+ abstract val selectionOptions: List
+ abstract val allowEmptyPrompt: Boolean
+ abstract val additionalImage: Bitmap?
+ abstract val imageLabels: List
- private val _isLoading = MutableStateFlow(false)
- val isLoading: StateFlow = _isLoading
+ private val _uiState = MutableStateFlow(ImagenUiState.Success())
+ val uiState: StateFlow = _uiState.asStateFlow()
- private val _generatedBitmaps = MutableStateFlow(listOf())
- val generatedBitmaps: StateFlow> = _generatedBitmaps
-
- // Firebase AI Logic
- private val imagenModel: ImagenModel
-
- init {
- val config = imagenGenerationConfig {
- numberOfImages = 4
- aspectRatio = ImagenAspectRatio.SQUARE_1x1
- imageFormat = ImagenImageFormat.png()
- }
- val settings = ImagenSafetySettings(
- safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
- personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
- )
- imagenModel = Firebase.ai(
- backend = GenerativeBackend.googleAI()
- ).imagenModel(
- modelName = sample.modelName ?: "imagen-3.0-generate-002",
- generationConfig = config,
- safetySettings = settings
- )
- }
+ protected abstract suspend fun performGeneration(
+ inputText: String,
+ currentState: ImagenUiState.Success
+ ): ImagenGenerationResponse
fun generateImages(inputText: String) {
+ val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
+
viewModelScope.launch {
- _isLoading.value = true
+ _uiState.value = ImagenUiState.Loading
try {
- val imageResponse = imagenModel.generateImages(
- inputText
- )
- _generatedBitmaps.value = imageResponse.images.map { it.asBitmap() }
- _errorMessage.value = null // clear error message
+ val imageResponse = performGeneration(inputText, currentState)
+ _uiState.value = currentState.copy(images = imageResponse.images.map { it.asBitmap() })
} catch (e: Exception) {
- _errorMessage.value = e.localizedMessage
- } finally {
- _isLoading.value = false
+ _uiState.value = ImagenUiState.Error(e.localizedMessage ?: "Unknown error")
}
}
}
+
+ suspend fun attachImage(
+ fileInBytes: ByteArray,
+ ) {
+ val originalBitmap = BitmapFactory.decodeByteArray(fileInBytes, 0, fileInBytes.size)
+ val resizedBitmap = originalBitmap.scale(
+ 512,
+ (originalBitmap.height * (512.0 / originalBitmap.width)).toInt()
+ )
+ val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
+ _uiState.value = currentState.copy(attachedImage = resizedBitmap)
+ }
+
+ fun selectOption(selection: String) {
+ val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
+ _uiState.value = currentState.copy(selectedOption = selection)
+ }
}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt
new file mode 100644
index 0000000000..1c225656db
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt
@@ -0,0 +1,51 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.ChatUiState
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object AudioSummarizationRoute
+
+class AudioSummarizationViewModel : ChatViewModel() {
+
+ override val initialPrompt: String =
+ """
+ I have attached the audio file. Please analyze it and summarize
+ the contents of the audio as bullet points.
+ """.trimIndent()
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-3.1-flash-lite-preview"
+ )
+ chat = generativeModel.startChat(
+ listOf(
+ content { text("Can you help me summarize an audio file?") },
+ content("model") {
+ text(
+ "Of course! Click on the attach button" +
+ " below and choose an audio file for me to summarize."
+ )
+ }
+ ))
+ _messages.value = chat.history.map { UiChatMessage(it) }
+ _uiState.value = ChatUiState.Success
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt
new file mode 100644
index 0000000000..9b83c8b085
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt
@@ -0,0 +1,39 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object AudioTranslationRoute
+
+class AudioTranslationViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "Please translate the audio to Mandarin."
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
+ modelName = "gemini-2.5-flash"
+ )
+ chat = generativeModel.startChat()
+
+ // Handling the initial fileData in the prompt builder for the first message
+ contentBuilder.fileData(
+ "https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/" +
+ "How_to_create_a_My_Map_in_Google_Maps.mp3",
+ "audio/mpeg"
+ )
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
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 665d2ef616..d33e948d68 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
@@ -1,117 +1,91 @@
package com.google.firebase.quickstart.ai.feature.text
import android.graphics.BitmapFactory
-import android.util.Log
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.toMutableStateList
-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.Chat
-import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
-import com.google.firebase.ai.type.FileDataPart
-import com.google.firebase.ai.type.FunctionResponsePart
import com.google.firebase.ai.type.GenerateContentResponse
-import com.google.firebase.ai.type.TextPart
-import com.google.firebase.ai.type.asTextOrNull
-import com.google.firebase.ai.type.content
-import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
-import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.quickstart.ai.ui.Attachment
+import com.google.firebase.quickstart.ai.ui.ChatUiState
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import kotlinx.serialization.json.jsonPrimitive
-
-class ChatViewModel(
- 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 _messageList: MutableList =
- sample.chatHistory.toMutableStateList()
- private val _messages = MutableStateFlow>(_messageList)
- val messages: StateFlow> =
- _messages
-
- private val _attachmentsList: MutableList =
- sample.initialPrompt?.parts?.filterIsInstance()?.map {
- Attachment(it.uri)
- }?.toMutableStateList() ?: mutableStateListOf()
- private val _attachments = MutableStateFlow>(_attachmentsList)
- val attachments: StateFlow>
- get() = _attachments
-
- // Firebase AI Logic
- private var contentBuilder = Content.Builder()
- private val chat: Chat
-
- init {
- val 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
- )
- chat = generativeModel.startChat(sample.chatHistory)
-
- // add attachments from initial prompt
- sample.initialPrompt?.parts?.forEach { part ->
- if (part is TextPart) {
- /* Ignore text parts, as the text will come from the textInputField */
- } else {
- contentBuilder.part(part)
- }
- }
- }
+@OptIn(PublicPreviewAPI::class)
+abstract class ChatViewModel : ViewModel() {
+
+ protected val _uiState = MutableStateFlow(ChatUiState.Success)
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ protected val _messages = MutableStateFlow>(emptyList())
+ val messages: StateFlow> = _messages.asStateFlow()
+
+ protected val _attachments = MutableStateFlow>(emptyList())
+ val attachments: StateFlow> = _attachments.asStateFlow()
+
+ abstract val initialPrompt: String
+
+ // Builder for the next message
+ protected var contentBuilder = Content.Builder()
+
+ /**
+ * Entry point for sending a message.
+ * Handles adding the message to the UI and setting the loading state.
+ */
fun sendMessage(userMessage: String) {
val prompt = contentBuilder
.text(userMessage)
.build()
- _messageList.add(prompt)
+ _messages.value = _messages.value + UiChatMessage(prompt)
viewModelScope.launch {
- _isLoading.value = true
+ _uiState.value = ChatUiState.Loading
try {
- val response = chat.sendMessage(prompt)
- if (response.functionCalls.isEmpty()) {
- // Samples without function calling can simply display
- // the response in the UI
- _messageList.add(response.candidates.first().content)
- } else {
- // Samples WITH function calling need to perform
- // additional handling
- handleFunctionCalls(response)
- }
- _errorMessage.value = null // clear errors
+ performSendMessage(prompt, _messages.value)
} catch (e: Exception) {
- _errorMessage.value = e.localizedMessage
+ _uiState.value = ChatUiState.Error(e.localizedMessage ?: "Unknown error")
} finally {
- _isLoading.value = false
contentBuilder = Content.Builder() // reset the builder
- _attachmentsList.clear()
}
}
}
+ /**
+ * Subclasses implement this to handle the actual AI logic.
+ */
+ protected abstract suspend fun performSendMessage(
+ prompt: Content,
+ currentMessages: List
+ )
+
+ /**
+ * Centralized method to validate the AI response (grounding check) and update the UI state.
+ */
+ protected fun validateAndDisplayResponse(
+ response: GenerateContentResponse,
+ currentMessages: List
+ ) {
+ val candidate = response.candidates.firstOrNull() ?: return
+
+ // Compliance check for grounding
+ if (candidate.groundingMetadata != null
+ && candidate.groundingMetadata?.groundingChunks?.isNotEmpty() == true
+ && candidate.groundingMetadata?.searchEntryPoint == null
+ ) {
+ _uiState.value = ChatUiState.Error(
+ "Could not display the response because it was missing required attribution components."
+ )
+ } else {
+ _messages.value = currentMessages + UiChatMessage(candidate.content, candidate.groundingMetadata)
+ _attachments.value = emptyList()
+ _uiState.value = ChatUiState.Success
+ }
+ }
+
fun attachFile(
fileInBytes: ByteArray,
mimeType: String?,
@@ -123,50 +97,10 @@ class ChatViewModel(
} else {
contentBuilder.inlineData(fileInBytes, mimeType ?: "text/plain")
}
- _attachmentsList.add(Attachment(fileName ?: "Unnamed file"))
- }
- /**
- * Only used by samples with function calling
- */
- private suspend fun handleFunctionCalls(
- response: GenerateContentResponse
- ) {
- response.functionCalls.forEach { functionCall ->
- Log.d(
- "ChatViewModel", "Model responded with function call:" +
- functionCall.name
- )
- when (functionCall.name) {
- "fetchWeather" -> {
- // Handle the call to fetchWeather()
- val city = functionCall.args["city"]!!.jsonPrimitive.content
- val state = functionCall.args["city"]!!.jsonPrimitive.content
- val date = functionCall.args["date"]!!.jsonPrimitive.content
-
- val functionResponse = WeatherRepository
- .fetchWeather(city, state, date)
-
- // Send the response(s) from the function back to the model
- // so that the model can use it to generate its final response.
- val finalResponse = chat.sendMessage(content("function") {
- part(FunctionResponsePart("fetchWeather", functionResponse))
- })
-
- Log.d("ChatViewModel", "Model responded with: ${finalResponse.text}")
- _messageList.add(finalResponse.candidates.first().content)
- }
-
- else -> {
- Log.d(
- "ChatViewModel", "Model responded with unknown" +
- " function call: ${functionCall.name}"
- )
- }
- }
- }
+ _attachments.value = _attachments.value + Attachment(fileName ?: "Unnamed file")
}
- private fun decodeBitmapFromImage(input: ByteArray) =
+ protected fun decodeBitmapFromImage(input: ByteArray) =
BitmapFactory.decodeByteArray(input, 0, input.size)
}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt
new file mode 100644
index 0000000000..4c96674516
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt
@@ -0,0 +1,42 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object CourseRecommendationsRoute
+
+class CourseRecommendationsViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "I am interested in Performing Arts. I have taken Theater 1A."
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash",
+ systemInstruction = content {
+ text(
+ "You are a chatbot for the county's performing and fine arts" +
+ " program. You help students decide what course they will" +
+ " take during the summer."
+ )
+ }
+ )
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt
new file mode 100644
index 0000000000..e91fb2fa0c
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt
@@ -0,0 +1,43 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object DocumentComparisonRoute
+
+class DocumentComparisonViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "The first document is from 2013, and the second document is" +
+ " from 2023. How did the standard deduction evolve?"
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
+ modelName = "gemini-2.5-flash"
+ )
+ chat = generativeModel.startChat()
+
+ // Pre-attach the documents
+ contentBuilder.fileData(
+ "https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2013.pdf",
+ "application/pdf"
+ )
+ contentBuilder.fileData(
+ "https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2023.pdf",
+ "application/pdf"
+ )
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt
new file mode 100644
index 0000000000..f2520878fa
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt
@@ -0,0 +1,36 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.Tool
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object GoogleSearchGroundingRoute
+
+class GoogleSearchGroundingViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "What's the weather in Chicago this weekend?"
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash",
+ tools = listOf(Tool.googleSearch())
+ )
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt
new file mode 100644
index 0000000000..8efc9bc42a
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt
@@ -0,0 +1,40 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object ImageBlogCreatorRoute
+
+class ImageBlogCreatorViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "Write a short, engaging blog post based on this picture." +
+ " It should include a description of the meal in the" +
+ " photo and talk about my journey meal prepping."
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
+ modelName = "gemini-2.5-flash"
+ )
+ chat = generativeModel.startChat()
+
+ // Pre-attach the image from cloud storage
+ contentBuilder.fileData(
+ "https://storage.googleapis.com/cloud-samples-data/generative-ai/image/meal-prep.jpeg",
+ "image/jpeg"
+ )
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt
new file mode 100644
index 0000000000..09c0802152
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt
@@ -0,0 +1,43 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.ResponseModality
+import com.google.firebase.ai.type.generationConfig
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object ImageGenerationRoute
+
+class ImageGenerationViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = """
+ Hi, can you create a 3d rendered image of a pig
+ with wings and a top hat flying over a happy
+ futuristic scifi city with lots of greenery?
+ """.trimIndent()
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash-image",
+ generationConfig = generationConfig {
+ responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
+ }
+ )
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt
new file mode 100644
index 0000000000..2fd85d6225
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt
@@ -0,0 +1,57 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.firebase.Firebase
+import com.google.firebase.ai.TemplateGenerativeModel
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.PublicPreviewAPI
+import com.google.firebase.quickstart.ai.ui.ServerPromptUiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ServerPromptTemplateRoute
+
+@OptIn(PublicPreviewAPI::class)
+class ServerPromptTemplateViewModel : ViewModel() {
+ val initialPrompt = "Jane Doe"
+ val allowEmptyPrompt = false
+
+ private val _uiState = MutableStateFlow(ServerPromptUiState.Success())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private var templateGenerativeModel: TemplateGenerativeModel
+
+ init {
+ templateGenerativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).templateGenerativeModel()
+ }
+
+ fun generate(inputText: String) {
+ viewModelScope.launch {
+ _uiState.value = ServerPromptUiState.Loading
+ try {
+ val response = templateGenerativeModel
+ .generateContent("input-system-instructions", mapOf("customerName" to inputText))
+ _uiState.value = ServerPromptUiState.Success(response.text)
+ } catch (e: Exception) {
+ _uiState.value = ServerPromptUiState.Error(
+ if (e.localizedMessage?.contains("not found") == true) {
+ """
+ Template was not found, please verify that your project contains a template
+ named "input-system-instructions".
+ """.trimIndent()
+ } else {
+ e.localizedMessage ?: "Unknown error"
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt
new file mode 100644
index 0000000000..bc6b4d7e22
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt
@@ -0,0 +1,71 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+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.content
+import com.google.firebase.ai.type.generationConfig
+import com.google.firebase.ai.type.thinkingConfig
+import kotlinx.coroutines.Dispatchers
+import com.google.firebase.quickstart.ai.ui.SvgUiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+@Serializable
+object SvgRoute
+
+class SvgViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(SvgUiState.Success())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private val generativeModel: GenerativeModel
+
+ init {
+ generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-3-flash-preview",
+ systemInstruction = 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()
+ )
+ },
+ generationConfig = generationConfig {
+ thinkingConfig {
+ thinkingBudget = -1
+ }
+ }
+ )
+ }
+
+ fun generateSVG(prompt: String) {
+ val currentSvgs = (_uiState.value as? SvgUiState.Success)?.svgs ?: emptyList()
+ _uiState.value = SvgUiState.Loading
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ val response = generativeModel.generateContent(prompt)
+ val newSvg = response.text
+ if (newSvg != null) {
+ _uiState.value = SvgUiState.Success(listOf(newSvg) + currentSvgs)
+ } else {
+ _uiState.value = SvgUiState.Success(currentSvgs)
+ }
+ } catch (e: Exception) {
+ _uiState.value = SvgUiState.Error(e.localizedMessage ?: "Unknown error")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt
new file mode 100644
index 0000000000..8b59f9e2a6
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt
@@ -0,0 +1,42 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.generationConfig
+import com.google.firebase.ai.type.thinkingConfig
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object ThinkingChatRoute
+
+class ThinkingChatViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "Analogize photosynthesis and growing up."
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash",
+ generationConfig = generationConfig {
+ thinkingConfig = thinkingConfig {
+ includeThoughts = true
+ thinkingBudget = -1 // Dynamic Thinking
+ }
+ }
+ )
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TranslationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TranslationViewModel.kt
new file mode 100644
index 0000000000..bc10192e9e
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TranslationViewModel.kt
@@ -0,0 +1,44 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+import kotlinx.serialization.Serializable
+
+@Serializable
+object TranslationRoute
+
+class TranslationViewModel : ChatViewModel() {
+ override val initialPrompt: String
+ get() = """
+ Translate the following text to Spanish:
+ Hey, are you down to grab some pizza later? I'm starving!
+ """.trimIndent()
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-3.1-flash-lite-preview",
+ systemInstruction = content {
+ text("Only output the translated text")
+ }
+ )
+
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(
+ prompt: Content,
+ currentMessages: List
+ ) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
\ No newline at end of file
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt
new file mode 100644
index 0000000000..dd3662f811
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt
@@ -0,0 +1,70 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.ChatUiState
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object TravelTipsRoute
+
+class TravelTipsViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "What else is important when traveling?"
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash",
+ systemInstruction = content {
+ text(
+ "You are a Travel assistant. You will answer" +
+ " questions the user asks based on the information listed" +
+ " in Relevant Information. Do not hallucinate. Do not use" +
+ " the internet."
+ )
+ }
+ )
+
+ chat = generativeModel.startChat(
+ history = listOf(
+ content("role") {
+ text("I have never traveled before. When should I book a flight?")
+ },
+ content("model") {
+ text(
+ "You should book flights a couple of months ahead of time." +
+ " It will be cheaper and more flexible for you."
+ )
+ },
+ content("user") {
+ text("Do I need a passport?")
+ },
+ content("model") {
+ text(
+ "If you are traveling outside your own country, make sure" +
+ " your passport is up-to-date and valid for more" +
+ " than 6 months during your travel."
+ )
+ }
+ )
+ )
+
+ _messages.value = chat.history.map { UiChatMessage(it) }
+ _uiState.value = ChatUiState.Success
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt
new file mode 100644
index 0000000000..ff1b1192b3
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt
@@ -0,0 +1,41 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object VideoHashtagGeneratorRoute
+
+class VideoHashtagGeneratorViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "Generate 5-10 hashtags that relate to the video content." +
+ " Try to use more popular and engaging terms," +
+ " e.g. #Viral. Do not add content not related to" +
+ " the video.\n Start the output with 'Tags:'"
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
+ modelName = "gemini-2.5-flash"
+ )
+ chat = generativeModel.startChat()
+
+ // Pre-attach the video
+ contentBuilder.fileData(
+ "https://storage.googleapis.com/cloud-samples-data/generative-ai/video/google_home_celebrity_ad.mp4",
+ "video/mpeg"
+ )
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt
new file mode 100644
index 0000000000..7c5bc64211
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt
@@ -0,0 +1,51 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GenerativeBackend
+import com.google.firebase.ai.type.content
+import com.google.firebase.quickstart.ai.ui.ChatUiState
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+
+@Serializable
+object VideoSummarizationRoute
+
+class VideoSummarizationViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "I have attached the video file. Provide a description of" +
+ " the video. The description should also contain" +
+ " anything important which people say in the video."
+
+ private val chat: Chat
+
+ init {
+ val chatHistory = listOf(
+ content { text("Can you help me with the description of a video file?") },
+ content("model") {
+ text(
+ "Sure! Click on the attach button below and choose a" +
+ " video file for me to describe."
+ )
+ }
+ )
+
+ _messages.value = chatHistory.map { UiChatMessage(it) }
+ _uiState.value = ChatUiState.Success
+
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash"
+ )
+ chat = generativeModel.startChat(chatHistory)
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ validateAndDisplayResponse(response, currentMessages)
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt
new file mode 100644
index 0000000000..f91998cf90
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt
@@ -0,0 +1,107 @@
+package com.google.firebase.quickstart.ai.feature.text
+
+import kotlinx.serialization.Serializable
+
+import android.util.Log
+import com.google.firebase.Firebase
+import com.google.firebase.ai.Chat
+import com.google.firebase.ai.ai
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.FunctionDeclaration
+import com.google.firebase.ai.type.FunctionResponsePart
+import com.google.firebase.ai.type.GenerateContentResponse
+import com.google.firebase.ai.type.GenerativeBackend
+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.quickstart.ai.feature.text.functioncalling.WeatherRepository
+import com.google.firebase.quickstart.ai.ui.UiChatMessage
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonPrimitive
+
+@Serializable
+object WeatherChatRoute
+
+class WeatherChatViewModel : ChatViewModel() {
+
+ override val initialPrompt: String = "What was the weather in Boston, MA on October 17, 2024?"
+
+ private val chat: Chat
+
+ init {
+ val generativeModel = Firebase.ai(
+ backend = GenerativeBackend.googleAI()
+ ).generativeModel(
+ modelName = "gemini-2.5-flash",
+ 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."
+ ),
+ ),
+ )
+ )
+ )
+ )
+ )
+ chat = generativeModel.startChat()
+ }
+
+ override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
+ val response = chat.sendMessage(prompt)
+ if (response.functionCalls.isEmpty()) {
+ validateAndDisplayResponse(response, currentMessages)
+ } else {
+ handleFunctionCalls(response, currentMessages)
+ }
+ }
+
+ private suspend fun handleFunctionCalls(
+ response: GenerateContentResponse,
+ currentMessages: List
+ ) {
+ response.functionCalls.forEach { functionCall ->
+ Log.d(
+ "WeatherChatViewModel", "Model responded with function call:" +
+ functionCall.name
+ )
+ when (functionCall.name) {
+ "fetchWeather" -> {
+ val city = functionCall.args["city"]?.jsonPrimitive?.content
+ val state = functionCall.args["state"]?.jsonPrimitive?.content // Fixed state retrieval
+ val date = functionCall.args["date"]?.jsonPrimitive?.content
+
+ val finalResponse = if (city == null || state == null || date == null) {
+ chat.sendMessage(content("function") {
+ part(FunctionResponsePart("fetchWeather",
+ JsonObject(
+ mapOf(
+ "error" to JsonPrimitive("Unable to fetch weather - one of the parameters was null"),
+ )
+ )))
+ })
+ } else {
+ val functionResponse = WeatherRepository
+ .fetchWeather(city, state, date)
+
+ chat.sendMessage(content("function") {
+ part(FunctionResponsePart("fetchWeather", functionResponse))
+ })
+ }
+
+ validateAndDisplayResponse(finalResponse, currentMessages)
+ }
+ }
+ }
+ }
+}
+
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/CameraView.kt
new file mode 100644
index 0000000000..d68c25cb3f
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/CameraView.kt
@@ -0,0 +1,98 @@
+package com.google.firebase.quickstart.ai.ui
+
+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.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+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.surfaceProvider = 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/text/ChatScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatScreen.kt
similarity index 63%
rename from firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt
rename to firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatScreen.kt
index b55cc89ced..683cfe29dc 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/ui/ChatScreen.kt
@@ -1,17 +1,26 @@
-package com.google.firebase.quickstart.ai.feature.text
+package com.google.firebase.quickstart.ai.ui
-import android.graphics.Bitmap
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Color
import android.net.Uri
import android.provider.OpenableColumns
import android.text.format.Formatter
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -22,15 +31,19 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Attachment
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
@@ -50,30 +63,32 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.FileDataPart
import com.google.firebase.ai.type.ImagePart
import com.google.firebase.ai.type.InlineDataPart
import com.google.firebase.ai.type.TextPart
+import com.google.firebase.ai.type.WebGroundingChunk
+import com.google.firebase.quickstart.ai.feature.text.ChatViewModel
import kotlinx.coroutines.launch
-import kotlinx.serialization.Serializable
-@Serializable
-class ChatRoute(val sampleId: String)
@Composable
fun ChatScreen(
- chatViewModel: ChatViewModel = viewModel()
+ chatViewModel: ChatViewModel
) {
- val messages: List by chatViewModel.messages.collectAsStateWithLifecycle()
- val isLoading: Boolean by chatViewModel.isLoading.collectAsStateWithLifecycle()
- val errorMessage: String? by chatViewModel.errorMessage.collectAsStateWithLifecycle()
- val attachments: List by chatViewModel.attachments.collectAsStateWithLifecycle()
+ val uiState by chatViewModel.uiState.collectAsStateWithLifecycle()
+ val messages by chatViewModel.messages.collectAsStateWithLifecycle()
+ val attachments by chatViewModel.attachments.collectAsStateWithLifecycle()
val initialPrompt: String = chatViewModel.initialPrompt
@@ -91,6 +106,7 @@ fun ChatScreen(
.fillMaxSize()
.weight(0.5f)
)
+
Box(
contentAlignment = Alignment.BottomCenter
) {
@@ -99,14 +115,14 @@ fun ChatScreen(
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
) {
- if (isLoading) {
+ if (uiState is ChatUiState.Loading) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
- errorMessage?.let {
+ (uiState as? ChatUiState.Error)?.let {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
@@ -114,7 +130,7 @@ fun ChatScreen(
modifier = Modifier.fillMaxWidth()
) {
Text(
- text = it,
+ text = it.message,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
@@ -153,26 +169,29 @@ fun ChatScreen(
chatViewModel.attachFile(bytes, mimeType, fileName)
}
},
- isLoading = isLoading
+ isLoading = uiState is ChatUiState.Loading
)
}
}
}
}
+@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun ChatBubbleItem(
- chatMessage: Content
+ message: UiChatMessage
) {
- val isModelMessage = chatMessage.role == "model"
+ val isModelMessage = message.content.role == "model"
- val backgroundColor = when (chatMessage.role) {
+ val isDarkTheme = isSystemInDarkTheme()
+
+ val backgroundColor = when (message.content.role) {
"user" -> MaterialTheme.colorScheme.tertiaryContainer
else -> MaterialTheme.colorScheme.secondaryContainer
}
val textColor = if (isModelMessage) {
- MaterialTheme.colorScheme.onSecondaryContainer
+ MaterialTheme.colorScheme.onBackground
} else {
MaterialTheme.colorScheme.onTertiaryContainer
}
@@ -196,7 +215,7 @@ fun ChatBubbleItem(
.fillMaxWidth()
) {
Text(
- text = chatMessage.role?.uppercase() ?: "USER",
+ text = message.content.role?.uppercase() ?: "USER",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 4.dp)
)
@@ -212,14 +231,18 @@ fun ChatBubbleItem(
.padding(16.dp)
.fillMaxWidth()
) {
- chatMessage.parts.forEach { part ->
+ message.content.parts.forEach { part ->
when (part) {
is TextPart -> {
- Text(
- text = part.text.trimIndent(),
- modifier = Modifier.fillMaxWidth(),
- color = textColor
- )
+ if (part.isThought) {
+ ThoughtBubble(part.text)
+ } else {
+ Text(
+ text = part.text.trimIndent(),
+ modifier = Modifier.fillMaxWidth(),
+ color = textColor
+ )
+ }
}
is ImagePart -> {
@@ -272,6 +295,57 @@ fun ChatBubbleItem(
}
}
}
+ message.groundingMetadata?.let { metadata ->
+ HorizontalDivider(modifier = Modifier.padding(vertical = 18.dp))
+
+ // Search Entry Point (WebView)
+ metadata.searchEntryPoint?.let { searchEntryPoint ->
+ val context = LocalContext.current
+ AndroidView(
+ factory = {
+ WebView(it).apply {
+ webViewClient = object : WebViewClient() {
+ override fun shouldOverrideUrlLoading(
+ view: WebView?,
+ request: WebResourceRequest?
+ ): Boolean {
+ request?.url?.let { uri ->
+ val intent = Intent(Intent.ACTION_VIEW, uri)
+ context.startActivity(intent)
+ }
+ // Return true to indicate we handled the URL loading
+ return true
+ }
+ }
+
+ setBackgroundColor(Color.TRANSPARENT)
+ loadDataWithBaseURL(
+ null,
+ searchEntryPoint.renderedContent,
+ "text/html",
+ "UTF-8",
+ null
+ )
+ }
+ },
+ modifier = Modifier
+ .clip(RoundedCornerShape(22.dp))
+ .fillMaxHeight()
+ .fillMaxWidth()
+ )
+ }
+
+ if (metadata.groundingChunks.isNotEmpty()) {
+ Text(
+ text = "Sources",
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
+ )
+ metadata.groundingChunks.forEach { chunk ->
+ chunk.web?.let { SourceLinkView(it) }
+ }
+ }
+ }
}
}
}
@@ -279,9 +353,41 @@ fun ChatBubbleItem(
}
}
+@Composable
+fun SourceLinkView(
+ webChunk: WebGroundingChunk
+) {
+ val context = LocalContext.current
+ val annotatedString = AnnotatedString.Builder(webChunk.title ?: "Untitled Source").apply {
+ addStyle(
+ style = SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ ),
+ start = 0,
+ end = webChunk.title?.length ?: "Untitled Source".length
+ )
+ webChunk.uri?.let { addStringAnnotation("URL", it, 0, it.length) }
+ }.toAnnotatedString()
+
+ Row(modifier = Modifier.padding(bottom = 8.dp)) {
+ Icon(
+ Icons.Default.Attachment,
+ contentDescription = "Source link",
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ ClickableText(text = annotatedString, onClick = { offset ->
+ annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
+ .firstOrNull()?.let { annotation ->
+ context.startActivity(Intent(Intent.ACTION_VIEW, annotation.item.toUri()))
+ }
+ })
+ }
+}
+
@Composable
fun ChatList(
- chatMessages: List,
+ chatMessages: List,
listState: LazyListState,
modifier: Modifier = Modifier
) {
@@ -424,22 +530,15 @@ fun AttachmentsMenu(
}
}
-/**
- * Meant to present attachments in the UI
- */
-data class Attachment(
- val fileName: String,
- val image: Bitmap? = null // only for image attachments
-)
@Composable
fun AttachmentsList(
attachments: List
) {
- LazyColumn(
+ Column(
modifier = Modifier.fillMaxWidth()
) {
- items(attachments) { attachment ->
+ attachments.forEach { attachment ->
Row(
modifier = Modifier
.fillMaxWidth()
@@ -452,22 +551,73 @@ fun AttachmentsList(
.padding(4.dp)
.align(Alignment.CenterVertically)
)
- attachment.image?.let {
- Image(
- bitmap = it.asImageBitmap(),
- contentDescription = attachment.fileName,
+ Column(modifier = Modifier.align (Alignment.CenterVertically)) {
+ attachment.image?.let {
+ Image(
+ bitmap = it.asImageBitmap(),
+ contentDescription = attachment.fileName,
+ modifier = Modifier
+ )
+ }
+ Text(
+ text = attachment.fileName,
+ style = MaterialTheme.typography.bodySmall,
modifier = Modifier
- .align(Alignment.CenterVertically)
+ .padding(horizontal = 4.dp)
)
}
+ }
+ }
+ }
+}
+
+@Composable
+fun ThoughtBubble(
+ text: String
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(MaterialTheme.colorScheme.tertiaryContainer)
+ .clickable { expanded = !expanded }
+ .padding(8.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(
+ imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ contentDescription = if (expanded) "Collapse thoughts" else "Expand thoughts",
+ modifier = Modifier.padding(end = 8.dp),
+ tint = MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ Text(
+ text = "Thoughts",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ }
+
+ AnimatedVisibility(visible = expanded) {
+ Column {
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = MaterialTheme.colorScheme.onTertiaryContainer
+ )
Text(
- text = attachment.fileName,
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier
- .align(Alignment.CenterVertically)
- .padding(horizontal = 4.dp)
+ text = text.trimIndent(),
+ style = MaterialTheme.typography.bodySmall.copy(
+ fontStyle = FontStyle.Italic
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
-}
+}
\ No newline at end of file
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatUiState.kt
new file mode 100644
index 0000000000..4176ca7f25
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatUiState.kt
@@ -0,0 +1,27 @@
+package com.google.firebase.quickstart.ai.ui
+
+import android.graphics.Bitmap
+import com.google.firebase.ai.type.Content
+import com.google.firebase.ai.type.GroundingMetadata
+
+/**
+ * Meant to present attachments in the UI
+ */
+data class Attachment(
+ val fileName: String,
+ val image: Bitmap? = null // only for image attachments
+)
+
+/**
+ * A wrapper for a model [Content] object that includes additional UI-specific metadata.
+ */
+data class UiChatMessage(
+ val content: Content,
+ val groundingMetadata: GroundingMetadata? = null,
+)
+
+sealed interface ChatUiState {
+ data object Loading : ChatUiState
+ data object Success : ChatUiState
+ data class Error(val message: String) : ChatUiState
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceScreen.kt
new file mode 100644
index 0000000000..dd04de6435
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceScreen.kt
@@ -0,0 +1,206 @@
+package com.google.firebase.quickstart.ai.ui
+
+import android.Manifest
+import android.content.pm.PackageManager
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.CameraAlt
+import androidx.compose.material.icons.filled.ReceiptLong
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel
+
+@Composable
+fun HybridInferenceScreen(
+ viewModel: HybridInferenceViewModel = viewModel()
+) {
+ val uiState by viewModel.uiState.collectAsState()
+ val context = LocalContext.current
+
+ val cameraLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.TakePicturePreview(),
+ onResult = { bitmap ->
+ bitmap?.let { viewModel.scanReceipt(it) }
+ }
+ )
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ cameraLauncher.launch(null)
+ }
+ }
+
+ Scaffold(
+ floatingActionButton = {
+ FloatingActionButton(
+ onClick = {
+ val permissionCheckResult =
+ ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
+ if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
+ cameraLauncher.launch(null)
+ } else {
+ permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ },
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ ) {
+ if (uiState.isScanning) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = MaterialTheme.colorScheme.onPrimary,
+ strokeWidth = 2.dp
+ )
+ } else {
+ Icon(Icons.Default.CameraAlt, contentDescription = "Scan Receipt")
+ }
+ }
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(16.dp)
+ ) {
+ // Model Status Card
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.ReceiptLong,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Spacer(modifier = Modifier.size(12.dp))
+ Column {
+ Text(
+ "Hybrid AI Status",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Text(
+ uiState.modelStatus,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ "Expenses",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ if (uiState.expenses.isEmpty()) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Text("No expenses yet. Scan a receipt to add one.", color = Color.Gray)
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(uiState.expenses) { expense ->
+ ExpenseItem(expense.name, expense.price, expense.inferenceMode)
+ }
+ }
+ }
+
+ if (uiState.errorMessage != null) {
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = uiState.errorMessage!!,
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ExpenseItem(name: String, price: Double, inferenceMode: String) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ name,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Medium
+ )
+ if (inferenceMode.isNotEmpty()) {
+ Text(
+ inferenceMode,
+ style = MaterialTheme.typography.labelSmall,
+ color = Color.Gray
+ )
+ }
+ }
+ Text(
+ "$${String.format("%.2f", price)}",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceUiState.kt
new file mode 100644
index 0000000000..eb61e6e2ab
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/HybridInferenceUiState.kt
@@ -0,0 +1,10 @@
+package com.google.firebase.quickstart.ai.ui
+
+import com.google.firebase.quickstart.ai.feature.hybrid.Expense
+
+data class HybridInferenceUiState(
+ val expenses: List = emptyList(),
+ val isScanning: Boolean = false,
+ val modelStatus: String = "Checking model status...",
+ val errorMessage: String? = null
+)
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenScreen.kt
new file mode 100644
index 0000000000..418d2a5ce3
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenScreen.kt
@@ -0,0 +1,254 @@
+package com.google.firebase.quickstart.ai.ui
+
+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 com.google.firebase.quickstart.ai.R
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel
+import kotlinx.coroutines.launch
+
+
+@Composable
+fun ImagenScreen(
+ imagenViewModel: ImagenViewModel
+) {
+ val uiState by imagenViewModel.uiState.collectAsStateWithLifecycle()
+ val successState = uiState as? ImagenUiState.Success
+ val attachedImage = successState?.attachedImage
+ val generatedImages = successState?.images ?: emptyList()
+
+ var imagenPrompt by rememberSaveable { mutableStateOf(imagenViewModel.initialPrompt) }
+
+ val context = LocalContext.current
+ val contentResolver = context.contentResolver
+ val scope = rememberCoroutineScope()
+ val openDocument = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { optionalUri: Uri? ->
+ optionalUri?.let { uri ->
+ var fileName: String? = null
+ // Fetch file name and size
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
+ cursor.moveToFirst()
+ val humanReadableSize = Formatter.formatShortFileSize(
+ context, cursor.getLong(sizeIndex)
+ )
+ fileName = "${cursor.getString(nameIndex)} ($humanReadableSize)"
+ }
+
+ contentResolver.openInputStream(uri)?.use { stream ->
+ val bytes = stream.readBytes()
+ scope.launch {
+ imagenViewModel.attachImage(bytes)
+ }
+ }
+ }
+ }
+
+ Column(
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
+ ElevatedCard(
+ modifier = Modifier
+ .padding(all = 16.dp)
+ .fillMaxWidth(),
+ shape = MaterialTheme.shapes.large
+ ) {
+ OutlinedTextField(
+ value = imagenPrompt,
+ label = { Text("Prompt") },
+ placeholder = { Text("Enter text to generate image") },
+ onValueChange = { imagenPrompt = it },
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth()
+ )
+ if (imagenViewModel.selectionOptions.isNotEmpty()) {
+ DropDownMenu(imagenViewModel.selectionOptions) { imagenViewModel.selectOption(it) }
+ }
+ val attachmentsList = buildList {
+ if (imagenViewModel.additionalImage != null) {
+ add(
+ Attachment(
+ imagenViewModel.imageLabels.getOrElse(0) { "" },
+ imagenViewModel.additionalImage
+ )
+ )
+ }
+ if (attachedImage != null) {
+ add(Attachment(imagenViewModel.imageLabels.getOrElse(1) { "" }, attachedImage))
+ }
+ }
+
+ if (imagenViewModel.includeAttach && attachmentsList.isNotEmpty()) {
+ AttachmentsList(attachmentsList)
+ }
+ Row() {
+ if (imagenViewModel.includeAttach) {
+ TextButton(
+ onClick = {
+ openDocument.launch(arrayOf("image/*"))
+ },
+ modifier = Modifier
+ .padding(end = 16.dp, bottom = 16.dp)
+ ) { Text("Attach") }
+ }
+ TextButton(
+ onClick = {
+ if (imagenViewModel.allowEmptyPrompt || imagenPrompt.isNotBlank()) {
+ imagenViewModel.generateImages(imagenPrompt)
+ }
+ },
+ modifier = Modifier
+ .padding(end = 16.dp, bottom = 16.dp)
+ ) {
+ Text("Generate")
+ }
+ }
+
+ }
+
+ if (uiState is ImagenUiState.Loading) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .padding(all = 8.dp)
+ .align(Alignment.CenterHorizontally)
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ (uiState as? ImagenUiState.Error)?.let {
+ Card(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ shape = MaterialTheme.shapes.large,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Text(
+ text = it.message,
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(all = 16.dp)
+ )
+ }
+ }
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(2),
+ modifier = Modifier
+ .padding(16.dp)
+ .height(500.dp)
+ ) {
+ items(generatedImages) { image ->
+ Card(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth(),
+ shape = MaterialTheme.shapes.large,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ ) {
+ Image(bitmap = image.asImageBitmap(), "Generated image")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DropDownMenu(items: List, onClick: (String) -> Unit) {
+
+ val isDropDownExpanded = remember {
+ mutableStateOf(false)
+ }
+
+ val itemPosition = remember {
+ mutableIntStateOf(0)
+ }
+
+
+ Column(
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ modifier = Modifier.padding(horizontal = 10.dp)
+ ) {
+
+ Box {
+ Row(
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.Top,
+ modifier = Modifier.clickable {
+ isDropDownExpanded.value = true
+ }
+ ) {
+ Text(text = items[itemPosition.intValue])
+ Image(
+ painter = painterResource(id = R.drawable.round_arrow_drop_down_24),
+ contentDescription = "Dropdown Icon"
+ )
+ }
+ DropdownMenu(
+ expanded = isDropDownExpanded.value,
+ onDismissRequest = {
+ isDropDownExpanded.value = false
+ }) {
+ items.forEachIndexed { index, item ->
+ DropdownMenuItem(
+ text = {
+ Text(text = item)
+ },
+ onClick = {
+ isDropDownExpanded.value = false
+ itemPosition.intValue = index
+ onClick(item)
+ })
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenUiState.kt
new file mode 100644
index 0000000000..907cabd134
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenUiState.kt
@@ -0,0 +1,14 @@
+package com.google.firebase.quickstart.ai.ui
+
+import android.graphics.Bitmap
+
+sealed interface ImagenUiState {
+ data object Idle : ImagenUiState
+ data object Loading : ImagenUiState
+ data class Success(
+ val images: List = emptyList(),
+ val attachedImage: Bitmap? = null,
+ val selectedOption: String? = null
+ ) : ImagenUiState
+ data class Error(val message: String) : ImagenUiState
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptScreen.kt
new file mode 100644
index 0000000000..a06cb91162
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptScreen.kt
@@ -0,0 +1,140 @@
+package com.google.firebase.quickstart.ai.ui
+
+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.padding
+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.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.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
+
+
+@Composable
+fun ServerPromptScreen(
+ viewModel: ServerPromptTemplateViewModel
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ val isLoading = uiState is ServerPromptUiState.Loading
+ val errorMessage = (uiState as? ServerPromptUiState.Error)?.message
+ val generatedText = (uiState as? ServerPromptUiState.Success)?.generatedText
+
+ ServerPromptContent(
+ initialPrompt = viewModel.initialPrompt,
+ isLoading = isLoading,
+ errorMessage = errorMessage,
+ generatedText = generatedText,
+ allowEmptyPrompt = viewModel.allowEmptyPrompt,
+ onGenerate = { viewModel.generate(it) }
+ )
+}
+
+@Composable
+private fun ServerPromptContent(
+ initialPrompt: String,
+ isLoading: Boolean,
+ errorMessage: String?,
+ generatedText: String?,
+ allowEmptyPrompt: Boolean,
+ onGenerate: (String) -> Unit
+) {
+ var textPrompt by rememberSaveable { mutableStateOf(initialPrompt) }
+
+ 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 (allowEmptyPrompt || textPrompt.isNotBlank()) {
+ onGenerate(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/ui/ServerPromptUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptUiState.kt
new file mode 100644
index 0000000000..d42d318b5a
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptUiState.kt
@@ -0,0 +1,8 @@
+package com.google.firebase.quickstart.ai.ui
+
+sealed interface ServerPromptUiState {
+ data object Idle : ServerPromptUiState
+ data object Loading : ServerPromptUiState
+ data class Success(val generatedText: String? = null) : ServerPromptUiState
+ data class Error(val message: String) : ServerPromptUiState
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeScreen.kt
new file mode 100644
index 0000000000..01df3111d9
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeScreen.kt
@@ -0,0 +1,142 @@
+package com.google.firebase.quickstart.ai.ui
+
+import android.Manifest
+import androidx.annotation.RequiresPermission
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CallEnd
+import androidx.compose.material.icons.filled.Mic
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.mutableStateOf
+
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+
+@RequiresPermission(Manifest.permission.RECORD_AUDIO)
+@Composable
+fun StreamRealtimeScreen(bidiView: BidiViewModel) {
+ val isConversationActive = remember { mutableStateOf(false) }
+ val backgroundColor =
+ MaterialTheme.colorScheme.background
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = backgroundColor
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ // The content will animate its size when it changes
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.animateContentSize()
+ ) {
+ if (isConversationActive.value) {
+ // Active state UI
+ Text(
+ text = "Conversation Active",
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Tap the end button to stop",
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else {
+ // Idle state UI
+ Text(
+ text = "Start Conversation",
+ fontSize = 22.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Tap the microphone to begin",
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(80.dp))
+
+ // The main button with pulsing animation
+ if (isConversationActive.value) {
+ // Button to end the conversation
+ IconButton(
+ onClick = {
+ bidiView.endConversation()
+ isConversationActive.value = false },
+ modifier = Modifier
+ .size(90.dp)
+ .clip(CircleShape),
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = Color(0xFFE63946), // A nice red color
+ contentColor = Color.White
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.CallEnd,
+ contentDescription = "End Conversation",
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ } else {
+ // Button to start the conversation
+ IconButton(
+ onClick = {
+ CoroutineScope(Dispatchers.IO).launch {
+ bidiView.startConversation()
+ }
+ isConversationActive.value = true },
+ modifier = Modifier
+ .size(90.dp)
+ .clip(CircleShape),
+ colors = IconButtonDefaults.iconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = Color.White
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Default.Mic,
+ contentDescription = "Start Conversation",
+ modifier = Modifier.size(48.dp)
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeVideoScreen.kt
new file mode 100644
index 0000000000..6b42a8754b
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeVideoScreen.kt
@@ -0,0 +1,81 @@
+package com.google.firebase.quickstart.ai.ui
+
+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 com.google.firebase.quickstart.ai.feature.live.BidiViewModel
+import kotlinx.coroutines.launch
+
+
+@RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA])
+@Composable
+fun StreamRealtimeVideoScreen(bidiView: BidiViewModel) {
+ 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/ImagenScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgScreen.kt
similarity index 61%
rename from firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenScreen.kt
rename to firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgScreen.kt
index e5d654a895..6f32f98248 100644
--- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenScreen.kt
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgScreen.kt
@@ -1,13 +1,12 @@
-package com.google.firebase.quickstart.ai.feature.media.imagen
+package com.google.firebase.quickstart.ai.ui
-import androidx.compose.foundation.Image
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.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
+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
@@ -23,27 +22,30 @@ 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.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
-import kotlinx.serialization.Serializable
-
-@Serializable
-class ImagenRoute(val sampleId: String)
+import coil3.compose.SubcomposeAsyncImage
+import coil3.request.ImageRequest
+import coil3.request.crossfade
+import coil3.svg.SvgDecoder
+import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
+import kotlinx.coroutines.Dispatchers
+import java.nio.ByteBuffer
@Composable
-fun ImagenScreen(
- imagenViewModel: ImagenViewModel = viewModel()
+fun SvgScreen(
+ svgViewModel: SvgViewModel
) {
- var imagenPrompt by rememberSaveable { mutableStateOf(imagenViewModel.initialPrompt) }
- val errorMessage by imagenViewModel.errorMessage.collectAsStateWithLifecycle()
- val isLoading by imagenViewModel.isLoading.collectAsStateWithLifecycle()
- val generatedImages by imagenViewModel.generatedBitmaps.collectAsStateWithLifecycle()
+ var prompt by rememberSaveable { mutableStateOf("A kitten") }
+ val uiState by svgViewModel.uiState.collectAsStateWithLifecycle()
+
+ val isLoading = uiState is SvgUiState.Loading
+ val errorMessage = (uiState as? SvgUiState.Error)?.message
+ val generatedSvgs = (uiState as? SvgUiState.Success)?.svgs ?: emptyList()
- Column(
- modifier = Modifier
- ) {
+ Column {
ElevatedCard(
modifier = Modifier
.padding(all = 16.dp)
@@ -51,28 +53,25 @@ fun ImagenScreen(
shape = MaterialTheme.shapes.large
) {
OutlinedTextField(
- value = imagenPrompt,
- label = { Text("Prompt") },
+ value = prompt,
+ label = { Text("Generate a SVG of") },
placeholder = { Text("Enter text to generate image") },
- onValueChange = { imagenPrompt = it },
+ onValueChange = { prompt = it },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
TextButton(
onClick = {
- if (imagenPrompt.isNotBlank()) {
- imagenViewModel.generateImages(imagenPrompt)
- }
+ svgViewModel.generateSVG(prompt)
},
modifier = Modifier
- .padding(end = 16.dp, bottom = 16.dp)
+ .padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.End)
) {
Text("Generate")
}
}
-
if (isLoading) {
Box(
contentAlignment = Alignment.Center,
@@ -83,6 +82,33 @@ fun ImagenScreen(
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
@@ -100,23 +126,5 @@ fun ImagenScreen(
)
}
}
- LazyVerticalGrid(
- columns = GridCells.Fixed(2),
- modifier = Modifier.padding(16.dp)
- ) {
- items(generatedImages) { image ->
- Card(
- modifier = Modifier
- .padding(8.dp)
- .fillMaxWidth(),
- shape = MaterialTheme.shapes.large,
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.onSecondaryContainer
- )
- ) {
- Image(bitmap = image.asImageBitmap(), "Generated image")
- }
- }
- }
}
-}
+}
\ No newline at end of file
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgUiState.kt
new file mode 100644
index 0000000000..a3126cf690
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgUiState.kt
@@ -0,0 +1,8 @@
+package com.google.firebase.quickstart.ai.ui
+
+sealed interface SvgUiState {
+ data object Idle : SvgUiState
+ data object Loading : SvgUiState
+ data class Success(val svgs: List = emptyList()) : SvgUiState
+ data class Error(val message: String) : SvgUiState
+}
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt
new file mode 100644
index 0000000000..a72196be2f
--- /dev/null
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt
@@ -0,0 +1,253 @@
+package com.google.firebase.quickstart.ai.ui.navigation
+
+import com.google.firebase.quickstart.ai.feature.live.StreamAudioViewModel
+import com.google.firebase.quickstart.ai.feature.live.StreamVideoViewModel
+import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceRoute
+import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel
+import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeAudioRoute
+import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenGenerationRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenGenerationViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenInpaintingRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenInpaintingViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenOutpaintingRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenOutpaintingViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenStyleTransferRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenStyleTransferViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenSubjectReferenceRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenSubjectReferenceViewModel
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenTemplateRoute
+import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenTemplateViewModel
+import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationRoute
+import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationViewModel
+import com.google.firebase.quickstart.ai.feature.text.AudioTranslationRoute
+import com.google.firebase.quickstart.ai.feature.text.AudioTranslationViewModel
+import com.google.firebase.quickstart.ai.feature.text.CourseRecommendationsRoute
+import com.google.firebase.quickstart.ai.feature.text.CourseRecommendationsViewModel
+import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonRoute
+import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonViewModel
+import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingRoute
+import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingViewModel
+import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorRoute
+import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorViewModel
+import com.google.firebase.quickstart.ai.feature.text.ImageGenerationRoute
+import com.google.firebase.quickstart.ai.feature.text.ImageGenerationViewModel
+import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateRoute
+import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
+import com.google.firebase.quickstart.ai.feature.text.SvgRoute
+import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
+import com.google.firebase.quickstart.ai.feature.text.ThinkingChatRoute
+import com.google.firebase.quickstart.ai.feature.text.ThinkingChatViewModel
+import com.google.firebase.quickstart.ai.feature.text.TranslationRoute
+import com.google.firebase.quickstart.ai.feature.text.TranslationViewModel
+import com.google.firebase.quickstart.ai.feature.text.TravelTipsRoute
+import com.google.firebase.quickstart.ai.feature.text.TravelTipsViewModel
+import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorRoute
+import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorViewModel
+import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationRoute
+import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationViewModel
+import com.google.firebase.quickstart.ai.feature.text.WeatherChatRoute
+import com.google.firebase.quickstart.ai.feature.text.WeatherChatViewModel
+
+val FIREBASE_AI_SAMPLES = listOf(
+ Sample(
+ title = "Translate text",
+ description = "Use Gemini 3.1 Flash-Lite to translate text",
+ route = TranslationRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = TranslationViewModel::class,
+ categories = listOf(Category.TEXT)
+ ),
+ Sample(
+ title = "Travel tips",
+ description = "The user wants the model to help a new traveler" +
+ " with travel tips",
+ route = TravelTipsRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = TravelTipsViewModel::class,
+ categories = listOf(Category.TEXT),
+ ),
+ Sample(
+ title = "Chatbot recommendations for courses",
+ description = "A chatbot suggests courses for a performing arts program.",
+ route = CourseRecommendationsRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = CourseRecommendationsViewModel::class,
+ categories = listOf(Category.TEXT),
+ ),
+ Sample(
+ title = "Audio Summarization",
+ description = "Use Gemini 3.1 Flash Lite to summarize an audio file",
+ route = AudioSummarizationRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = AudioSummarizationViewModel::class,
+ categories = listOf(Category.AUDIO),
+ ),
+ Sample(
+ title = "Translation from audio (Vertex AI)",
+ description = "Translate an audio file stored in Cloud Storage",
+ route = AudioTranslationRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = AudioTranslationViewModel::class,
+ categories = listOf(Category.AUDIO)
+ ),
+ Sample(
+ title = "Blog post creator (Vertex AI)",
+ description = "Create a blog post from an image file stored in Cloud Storage.",
+ route = ImageBlogCreatorRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = ImageBlogCreatorViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Imagen 4 - image generation",
+ description = "Generate images using Imagen 4",
+ route = ImagenGenerationRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenGenerationViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Imagen 3 - Inpainting (Vertex AI)",
+ description = "Replace part of an image using Imagen 3",
+ route = ImagenInpaintingRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenInpaintingViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Imagen 3 - Outpainting (Vertex AI)",
+ description = "Expand an image by drawing in more background",
+ route = ImagenOutpaintingRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenOutpaintingViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Imagen 3 - Subject Reference (Vertex AI)",
+ description = "Generate an image using a referenced subject (must be an animal)",
+ route = ImagenSubjectReferenceRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenSubjectReferenceViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Imagen 3 - Style Transfer (Vertex AI)",
+ description = "Change the art style of a cat picture using a reference",
+ route = ImagenStyleTransferRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenStyleTransferViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Gemini 2.5 Flash Image (aka nanobanana)",
+ description = "Generate and/or edit images using Gemini 2.5 Flash Image aka nanobanana",
+ route = ImageGenerationRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = ImageGenerationViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ Sample(
+ title = "Document comparison (Vertex AI)",
+ description = "Compare the contents of 2 documents." +
+ " Only supported by the Vertex AI Gemini API because the documents are stored in Cloud Storage",
+ route = DocumentComparisonRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = DocumentComparisonViewModel::class,
+ categories = listOf(Category.DOCUMENT)
+ ),
+ Sample(
+ title = "Hashtags for a video (Vertex AI)",
+ description = "Generate hashtags for a video ad stored in Cloud Storage",
+ route = VideoHashtagGeneratorRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = VideoHashtagGeneratorViewModel::class,
+ categories = listOf(Category.VIDEO)
+ ),
+ Sample(
+ title = "Summarize video",
+ description = "Summarize a video and extract important dialogue.",
+ route = VideoSummarizationRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = VideoSummarizationViewModel::class,
+ categories = listOf(Category.VIDEO)
+ ),
+ Sample(
+ title = "ForecastTalk",
+ description = "Use bidirectional streaming to get information about" +
+ " weather conditions for a specific US city on a specific date",
+ route = StreamRealtimeAudioRoute,
+ screenType = ScreenType.BIDI,
+ viewModelClass = StreamAudioViewModel::class,
+ categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING)
+ ),
+ Sample(
+ title = "Gemini Live (Video input)",
+ description = "Use bidirectional streaming to chat with Gemini using your" +
+ " phone's camera",
+ route = StreamRealtimeVideoRoute,
+ screenType = ScreenType.BIDI_VIDEO,
+ viewModelClass = StreamVideoViewModel::class,
+ categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING)
+ ),
+ Sample(
+ title = "Weather Chat",
+ description = "Use function calling to get the weather conditions" +
+ " for a specific US city on a specific date.",
+ route = WeatherChatRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = WeatherChatViewModel::class,
+ categories = listOf(Category.TEXT, Category.FUNCTION_CALLING)
+ ),
+ Sample(
+ title = "Grounding with Google Search",
+ description = "Use Grounding with Google Search to get responses based on up-to-date information from the" +
+ " web.",
+ route = GoogleSearchGroundingRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = GoogleSearchGroundingViewModel::class,
+ categories = listOf(Category.TEXT)
+ ),
+ 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.",
+ route = ImagenTemplateRoute,
+ screenType = ScreenType.IMAGEN,
+ viewModelClass = ImagenTemplateViewModel::class,
+ categories = listOf(Category.IMAGE)
+ ),
+ 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.",
+ route = ServerPromptTemplateRoute,
+ screenType = ScreenType.SERVER_PROMPT,
+ viewModelClass = ServerPromptTemplateViewModel::class,
+ categories = listOf(Category.TEXT),
+ ),
+ Sample(
+ title = "Thinking",
+ description = "Gemini 2.5 Flash with dynamic thinking",
+ route = ThinkingChatRoute,
+ screenType = ScreenType.CHAT,
+ viewModelClass = ThinkingChatViewModel::class,
+ categories = listOf(Category.TEXT)
+ ),
+ Sample(
+ title = "SVG Generator",
+ description = "Use Gemini 3 Flash preview to create SVG illustrations",
+ route = SvgRoute,
+ screenType = ScreenType.SVG,
+ viewModelClass = SvgViewModel::class,
+ categories = listOf(Category.IMAGE, Category.TEXT)
+ ),
+ Sample(
+ title = "Hybrid Receipt Scanner",
+ description = "Use hybrid inference to scan receipts and extract expense data on-device whenever possible.",
+ route = HybridInferenceRoute,
+ screenType = ScreenType.HYBRID,
+ viewModelClass = HybridInferenceViewModel::class,
+ categories = listOf(Category.TEXT, Category.IMAGE, Category.HYBRID)
+ )
+)
diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/MainMenuScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/MainMenuScreen.kt
index aa14ed497c..74a9f9689a 100644
--- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/MainMenuScreen.kt
+++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/MainMenuScreen.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES
val MIN_CARD_SIZE = 180.dp
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 903f6a1114..a51b56315b 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
@@ -1,10 +1,7 @@
package com.google.firebase.quickstart.ai.ui.navigation
-import com.google.firebase.ai.type.Content
-import com.google.firebase.ai.type.GenerationConfig
-import com.google.firebase.ai.type.GenerativeBackend
-import com.google.firebase.ai.type.Tool
-import java.util.UUID
+import androidx.lifecycle.ViewModel
+import kotlin.reflect.KClass
enum class Category(
val label: String
@@ -15,20 +12,25 @@ enum class Category(
AUDIO("Audio"),
DOCUMENT("Document"),
FUNCTION_CALLING("Function calling"),
+ LIVE_API("Live API Streaming"),
+ HYBRID("Hybrid inference")
+}
+
+enum class ScreenType {
+ CHAT,
+ IMAGEN,
+ SVG,
+ SERVER_PROMPT,
+ BIDI,
+ BIDI_VIDEO,
+ HYBRID
}
data class Sample(
- val id: String = UUID.randomUUID().toString(), // used for navigation
val title: String,
val description: String,
- val navRoute: String,
+ val route: Any,
+ val screenType: ScreenType,
+ val viewModelClass: KClass? = null,
val categories: List,
- // Optional parameters
- val modelName: String? = null,
- val backend: GenerativeBackend = GenerativeBackend.googleAI(),
- val initialPrompt: Content? = null,
- val systemInstructions: Content? = null,
- val generationConfig: GenerationConfig? = null,
- val chatHistory: List = emptyList(),
- val tools: List? = null
)
diff --git a/firebase-ai/app/src/main/res/drawable/cat.jpeg b/firebase-ai/app/src/main/res/drawable/cat.jpeg
new file mode 100644
index 0000000000..28bacdcbec
Binary files /dev/null and b/firebase-ai/app/src/main/res/drawable/cat.jpeg differ
diff --git a/firebase-ai/app/src/main/res/drawable/round_arrow_drop_down_24.xml b/firebase-ai/app/src/main/res/drawable/round_arrow_drop_down_24.xml
new file mode 100644
index 0000000000..d1e8d534d0
--- /dev/null
+++ b/firebase-ai/app/src/main/res/drawable/round_arrow_drop_down_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
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/app/src/main/res/values/themes.xml b/firebase-ai/app/src/main/res/values/themes.xml
index d82976cfb7..35071aca45 100644
--- a/firebase-ai/app/src/main/res/values/themes.xml
+++ b/firebase-ai/app/src/main/res/values/themes.xml
@@ -1,5 +1,6 @@
-
+
+
\ No newline at end of file
diff --git a/firebase-ai/build.gradle.kts b/firebase-ai/build.gradle.kts
index f9e8c0a44f..11ea2d1c62 100644
--- a/firebase-ai/build.gradle.kts
+++ b/firebase-ai/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" apply false
- id("org.jetbrains.kotlin.plugin.serialization") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.google.services) apply false
}
diff --git a/firebase-ai/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml
deleted file mode 100644
index d86e7a097e..0000000000
--- a/firebase-ai/gradle/libs.versions.toml
+++ /dev/null
@@ -1,44 +0,0 @@
-[versions]
-activityCompose = "1.10.1"
-agp = "8.9.2"
-composeBom = "2024.09.00"
-composeNavigation = "2.9.2"
-coreKtx = "1.16.0"
-espressoCore = "3.6.1"
-firebaseBom = "33.16.0"
-junit = "4.13.2"
-junitVersion = "1.2.1"
-kotlin = "2.0.21"
-kotlinxSerializationCore = "1.9.0"
-lifecycle = "2.9.2"
-lifecycleRuntimeKtx = "2.8.7"
-
-[libraries]
-androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
-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-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
-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" }
-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" }
-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-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" }
-
-[plugins]
-android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
-kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/firebase-ai/gradle/wrapper/gradle-wrapper.properties b/firebase-ai/gradle/wrapper/gradle-wrapper.properties
index 06f5495091..289e22e5e2 100644
--- a/firebase-ai/gradle/wrapper/gradle-wrapper.properties
+++ b/firebase-ai/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Apr 30 20:17:52 BST 2025
+#Tue Aug 19 11:04:48 PDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/firebase-ai/settings.gradle.kts b/firebase-ai/settings.gradle.kts
index f1cb15d710..321397594b 100644
--- a/firebase-ai/settings.gradle.kts
+++ b/firebase-ai/settings.gradle.kts
@@ -14,9 +14,17 @@ pluginManagement {
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
+ mavenLocal()
google()
mavenCentral()
}
+ dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+ }
}
rootProject.name = "Firebase AI Logic"
diff --git a/firestore/app/build.gradle.kts b/firestore/app/build.gradle.kts
index 6b4bf04a89..c38d9062e7 100644
--- a/firestore/app/build.gradle.kts
+++ b/firestore/app/build.gradle.kts
@@ -1,8 +1,8 @@
+
plugins {
- id("com.android.application")
- id("kotlin-android")
- id("com.google.gms.google-services")
- id("androidx.navigation.safeargs")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.google.services)
+ alias(libs.plugins.navigation.safeargs)
}
android {
@@ -24,8 +24,8 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- testProguardFiles(getDefaultProguardFile("proguard-android.txt"), "test-proguard-rules.pro")
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "test-proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
@@ -33,9 +33,6 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -54,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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firestore
implementation("com.google.firebase:firebase-firestore")
@@ -63,39 +60,37 @@ dependencies {
implementation("com.google.firebase:firebase-auth")
// Google Play services
- // Pinned to 20.7.0 as a workaround for issue https://github.com/firebase/quickstart-android/issues/1647
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.10.1")
+ implementation("androidx.activity:activity-ktx:1.12.1")
implementation("androidx.appcompat:appcompat:1.7.1")
- implementation("androidx.core:core-ktx:1.16.0")
+ implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.vectordrawable:vectordrawable-animated:1.2.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.browser:browser:1.5.0")
- implementation("com.google.android.material:material:1.12.0")
- implementation("androidx.media:media:1.7.0")
+ implementation("com.google.android.material:material:1.13.0")
+ 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.2")
- implementation("androidx.navigation:navigation-ui-ktx:2.9.2")
+ 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.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
- annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.9.2")
+ annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.10.0")
// Third-party libraries
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
implementation("com.github.bumptech.glide:glide:4.12.0")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test.espresso:espresso-contrib:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test.espresso:espresso-contrib:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("junit:junit:4.13.2")
androidTestImplementation("org.hamcrest:hamcrest-library:3.0")
diff --git a/firestore/app/src/main/AndroidManifest.xml b/firestore/app/src/main/AndroidManifest.xml
index 74084c7251..0f998236e7 100644
--- a/firestore/app/src/main/AndroidManifest.xml
+++ b/firestore/app/src/main/AndroidManifest.xml
@@ -5,8 +5,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
- android:theme="@style/AppTheme"
- android:name="androidx.multidex.MultiDexApplication">
+ android:theme="@style/AppTheme">
= 0.4"
+ }
+ },
"node_modules/capitalize-sentence": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/capitalize-sentence/-/capitalize-sentence-0.1.5.tgz",
@@ -848,6 +861,20 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
@@ -903,13 +930,10 @@
}
},
"node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
"engines": {
"node": ">= 0.4"
}
@@ -923,6 +947,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1036,24 +1088,38 @@
"license": "MIT",
"optional": true
},
- "node_modules/fast-xml-parser": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
- "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
+ "node_modules/fast-xml-builder": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
+ "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
- },
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "path-expression-matcher": "^1.1.3"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.5.7",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz",
+ "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==",
+ "funding": [
{
- "type": "paypal",
- "url": "https://paypal.me/naturalintelligence"
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
- "strnum": "^1.0.5"
+ "fast-xml-builder": "^1.1.4",
+ "path-expression-matcher": "^1.1.3",
+ "strnum": "^2.2.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -1151,15 +1217,17 @@
}
},
"node_modules/form-data": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz",
- "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+ "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"optional": true,
"dependencies": {
"asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
@@ -1256,16 +1324,21 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1274,6 +1347,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/google-auth-library": {
"version": "9.14.2",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.2.tgz",
@@ -1331,12 +1417,12 @@
}
},
"node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.1.3"
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1368,10 +1454,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1380,11 +1466,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -1576,34 +1666,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 +1716,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"
}
},
@@ -1724,6 +1814,15 @@
"lru-cache": "6.0.0"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -1821,9 +1920,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"
@@ -1906,6 +2005,22 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
+ "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
@@ -2269,9 +2384,15 @@
}
},
"node_modules/strnum": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
- "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz",
+ "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
"license": "MIT",
"optional": true
},
diff --git a/functions/gradle/wrapper/gradle-wrapper.properties b/functions/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/functions/gradle/wrapper/gradle-wrapper.properties
+++ b/functions/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradle.properties b/gradle.properties
index 3c231b0810..93387cb63e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,6 +6,5 @@ org.gradle.caching=true
## Play and Firebase moving to AndroidX
android.useAndroidX=true
-android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0812b49eb6..5fc9de341f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,50 +1,73 @@
[versions]
-agp = "8.11.1"
-coilCompose = "2.7.0"
-firebaseBom = "33.16.0"
-kotlin = "2.2.0"
-coreKtx = "1.16.0"
+activityCompose = "1.12.1"
+agp = "9.0.0"
+camerax = "1.5.2"
+coil3Compose = "3.3.0"
+composeBom = "2025.12.00"
+composeNavigation = "2.9.6"
+coreKtx = "1.17.0"
+espressoCore = "3.7.0"
+firebaseBom = "34.11.0"
+googleServices = "4.4.4"
+firebaseCrashlytics = "3.0.6"
+firebasePerf = "2.0.2"
+gradleVersions = "0.53.0"
junit = "4.13.2"
-junitVersion = "1.2.1"
-espressoCore = "3.6.1"
+junitVersion = "1.3.0"
+kotlin = "2.3.0"
kotlinxSerializationCore = "1.9.0"
-lifecycle = "2.9.2"
-activityCompose = "1.10.1"
-composeBom = "2025.07.00"
-googleServices = "4.4.3"
-composeNavigation = "2.9.2"
+lifecycle = "2.10.0"
+material = "1.13.0"
+materialIcons = "1.7.8"
+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" }
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.coil3:coil-compose", version.ref = "coil3Compose" }
+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" }
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"}
+firebase-ai = { module = "com.google.firebase:firebase-ai" }
+firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta01" }
+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" }
+material = { module = "com.google.android.material:material", version.ref = "material" }
+compose-material-icons = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "materialIcons"}
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" }
+firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerf" }
+navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "composeNavigation" }
+gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index c1962a79e2..a4b76b9530 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index aa02b02fc6..19a6bdeb84 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index aeb74cbb43..f3b75f3b0d 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -130,10 +133,13 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
@@ -141,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -149,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,11 +204,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/gradlew.bat b/gradlew.bat
index 6689b85bee..9b42019c79 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
diff --git a/inappmessaging/app/build.gradle.kts b/inappmessaging/app/build.gradle.kts
index f55c855cba..2fcc93c3b9 100644
--- a/inappmessaging/app/build.gradle.kts
+++ b/inappmessaging/app/build.gradle.kts
@@ -1,6 +1,6 @@
+
plugins {
id("com.android.application")
- id("kotlin-android")
id("com.google.gms.google-services")
}
@@ -9,7 +9,7 @@ android {
compileSdk = 36
defaultConfig {
applicationId = "com.google.firebase.fiamquickstart"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -21,16 +21,13 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -47,12 +44,11 @@ dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
- implementation("com.google.android.material:material:1.12.0")
+ implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
- 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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// FIAM
implementation("com.google.firebase:firebase-inappmessaging-display")
@@ -61,10 +57,10 @@ dependencies {
// Analytics
implementation("com.google.firebase:firebase-analytics")
- implementation("com.google.firebase:firebase-installations:18.0.0")
+ implementation("com.google.firebase:firebase-installations:19.0.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
}
diff --git a/inappmessaging/app/src/main/AndroidManifest.xml b/inappmessaging/app/src/main/AndroidManifest.xml
index f9816bcd6e..b8ae535732 100644
--- a/inappmessaging/app/src/main/AndroidManifest.xml
+++ b/inappmessaging/app/src/main/AndroidManifest.xml
@@ -2,7 +2,6 @@
{
@@ -43,9 +36,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
+
buildFeatures {
viewBinding = true
}
@@ -61,17 +52,17 @@ 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:33.16.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.12.0")
+ implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("com.github.bumptech.glide:glide:4.12.0")
testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}
diff --git a/perf/build.gradle.kts b/perf/build.gradle.kts
index 4ac0a58f09..e4fcb4d0b1 100644
--- a/perf/build.gradle.kts
+++ b/perf/build.gradle.kts
@@ -1,11 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
- id("com.google.firebase.firebase-perf") version "1.4.2" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
+ alias(libs.plugins.firebase.perf) apply false
}
allprojects {
@@ -18,6 +17,6 @@ allprojects {
tasks {
register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
}
diff --git a/perf/gradle/wrapper/gradle-wrapper.properties b/perf/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/perf/gradle/wrapper/gradle-wrapper.properties
+++ b/perf/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/scripts/ci_remove_fdc.py b/scripts/ci_remove_fdc.py
deleted file mode 100644
index c425af615b..0000000000
--- a/scripts/ci_remove_fdc.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# TODO(thatfiredev): remove this once github.com/firebase/quickstart-android/issues/1672 is fixed
-with open('settings.gradle.kts', 'r') as file:
- filedata = file.read()
-
-filedata = filedata.replace('":dataconnect:app",', '')
-
-with open('settings.gradle.kts', 'w') as file:
- file.write(filedata)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f18b8b8720..38bf5ce183 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -15,7 +15,6 @@ include(":admob:app",
":crash:app",
":database:app",
":dataconnect:app",
- ":dynamiclinks:app",
":firestore:app",
":functions:app",
":internal:chooserx",
@@ -24,6 +23,5 @@ include(":admob:app",
":inappmessaging:app",
":messaging:app",
":perf:app",
- ":storage:app",
- ":vertexai:app"
+ ":storage:app"
)
diff --git a/storage/app/build.gradle.kts b/storage/app/build.gradle.kts
index 8af87594d6..c003fbf439 100644
--- a/storage/app/build.gradle.kts
+++ b/storage/app/build.gradle.kts
@@ -2,7 +2,6 @@ import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
id("com.android.application")
- id("kotlin-android")
id("com.google.gms.google-services")
}
@@ -27,16 +26,13 @@ android {
buildTypes {
getByName("release") {
isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
viewBinding = true
@@ -48,7 +44,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:33.16.0"))
+ implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Cloud Storage for Firebase
implementation("com.google.firebase:firebase-storage")
@@ -56,11 +52,11 @@ dependencies {
// Firebase Authentication
implementation("com.google.firebase:firebase-auth")
- implementation("androidx.activity:activity-ktx:1.10.1")
+ implementation("androidx.activity:activity-ktx:1.12.1")
implementation("androidx.appcompat:appcompat:1.7.1")
- implementation("com.google.android.material:material:1.12.0")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1")
- androidTestImplementation("androidx.test:rules:1.6.1")
- androidTestImplementation("androidx.test:runner:1.6.2")
+ implementation("com.google.android.material:material:1.13.0")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
+ androidTestImplementation("androidx.test.espresso:espresso-intents:3.7.0")
+ androidTestImplementation("androidx.test:rules:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
}
diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts
index e1a62482c1..81a82fd0d4 100644
--- a/storage/build.gradle.kts
+++ b/storage/build.gradle.kts
@@ -1,10 +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.11.1" apply false
- id("com.android.library") version "8.11.1" apply false
- id("org.jetbrains.kotlin.android") version "2.2.0" apply false
- id("com.google.gms.google-services") version "4.4.3" apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.google.services) apply false
}
allprojects {
@@ -17,6 +16,6 @@ allprojects {
tasks {
register("clean", Delete::class) {
- delete(rootProject.buildDir)
+ delete(rootProject.layout.buildDirectory)
}
}
diff --git a/storage/gradle/wrapper/gradle-wrapper.properties b/storage/gradle/wrapper/gradle-wrapper.properties
index 3ae1e2f124..5dc98dbcf3 100644
--- a/storage/gradle/wrapper/gradle-wrapper.properties
+++ b/storage/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists