Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.google.ksp)
alias(libs.plugins.google.hilt)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.firebase.perf)
}

android {
Expand Down Expand Up @@ -63,10 +64,12 @@ dependencies {

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)
implementation(libs.firebase.ai.ondevice)
implementation(libs.firebase.config)
implementation(libs.firebase.auth)
implementation(libs.firebase.storage)
implementation(libs.firebase.firestore)
implementation(libs.firebase.perf)

//Library to handle Markdown in Compose
implementation(libs.richtext.commonmark)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,65 @@
package com.google.firebase.example.friendlymeals.data.datasource

import android.graphics.Bitmap
import android.util.Log
import com.google.firebase.Firebase
import com.google.firebase.ai.FirebaseAI
import com.google.firebase.ai.InferenceMode
import com.google.firebase.ai.InferenceSource
import com.google.firebase.ai.OnDeviceConfig
import com.google.firebase.ai.TemplateGenerativeModel
import com.google.firebase.ai.TemplateImagenModel
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.ImagePart
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.content
import com.google.firebase.example.friendlymeals.data.schema.MealSchema
import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema
import com.google.firebase.perf.performance
import com.google.firebase.perf.trace
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import kotlinx.serialization.json.Json
import javax.inject.Inject

@OptIn(PublicPreviewAPI::class)
class AIRemoteDataSource @Inject constructor(
private val aiModel: FirebaseAI,
private val generativeModel: TemplateGenerativeModel,
private val imagenModel: TemplateImagenModel,
private val remoteConfig: FirebaseRemoteConfig
) {
private val json = Json { ignoreUnknownKeys = true }
private val hybridGenerativeModel = aiModel.generativeModel(
modelName = remoteConfig.getString(HYBRID_CLOUD_MODEL_KEY),
onDeviceConfig = OnDeviceConfig(mode = InferenceMode.PREFER_IN_CLOUD)
)

suspend fun generateIngredients(imageData: String): String {
val response = generativeModel.generateContent(
templateId = remoteConfig.getString(GENERATE_INGREDIENTS_KEY),
inputs = mapOf(
MIME_TYPE_FIELD to MIME_TYPE_VALUE,
IMAGE_DATA_FIELD to imageData
@OptIn(PublicPreviewAPI::class)
suspend fun generateIngredients(image: Bitmap): String {
// Adding a Performance Monitoring trace is completely optional. Traces can help you
// measure how long it takes to generate ingredients on device and in cloud.
Firebase.performance.newTrace("hybrid-inference").trace {
val prompt = content {
image(image)
text(remoteConfig.getString(HYBRID_INGREDIENTS_PROMPT_KEY))
}

val response = hybridGenerativeModel.generateContent(prompt)

// This is an optional function that adds an attribute to the Performance Monitoring
// trace. It helps you identify the source of the inference.
putAttribute(
"inferenceSource",
when (response.inferenceSource) {
InferenceSource.ON_DEVICE -> "On device"
else -> "In cloud"
}
)
)

return response.text.orEmpty()
return response.text.orEmpty()
}
}

suspend fun generateRecipe(ingredients: String, notes: String): RecipeSchema? {
Expand Down Expand Up @@ -80,13 +111,45 @@ class AIRemoteDataSource @Inject constructor(
}
}

suspend fun loadOnDeviceModel() {
when (FirebaseAIOnDevice.checkStatus()) {
OnDeviceModelStatus.UNAVAILABLE -> {
Log.i(TAG, "On-device model is unavailable")
}
OnDeviceModelStatus.DOWNLOADABLE -> {
FirebaseAIOnDevice.download().collect { status ->
when (status) {
is DownloadStatus.DownloadStarted ->
Log.i(TAG, "Starting download - ${status.bytesToDownload}")

is DownloadStatus.DownloadInProgress ->
Log.i(TAG, "Download in progress ${status.totalBytesDownloaded} bytes downloaded")

is DownloadStatus.DownloadCompleted ->
Log.i(TAG, "On-device model download complete")

is DownloadStatus.DownloadFailed ->
Log.e(TAG, "Download failed $status")
}
}
}
OnDeviceModelStatus.DOWNLOADING -> {
Log.i(TAG, "On-device model is being downloaded")
}
OnDeviceModelStatus.AVAILABLE -> {
Log.i(TAG, "On-device model is available")
}
}
}

companion object {
//Remote Config Keys
private const val GENERATE_INGREDIENTS_KEY = "generate_ingredients"
private const val GENERATE_RECIPE_KEY = "generate_recipe"
private const val GENERATE_RECIPE_PHOTO_GEMINI_KEY = "generate_recipe_photo_gemini"
private const val GENERATE_RECIPE_PHOTO_IMAGEN_KEY = "generate_recipe_photo_imagen"
private const val SCAN_MEAL_KEY = "scan_meal"
private const val HYBRID_CLOUD_MODEL_KEY = "hybrid_cloud_model"
private const val HYBRID_INGREDIENTS_PROMPT_KEY = "hybrid_ingredients_prompt"

//Template input fields
private const val IMAGE_DATA_FIELD = "imageData"
Expand All @@ -97,5 +160,8 @@ class AIRemoteDataSource @Inject constructor(

//Template input values
private const val MIME_TYPE_VALUE = "image/jpeg"

//Class TAG
private const val TAG = "AIRemoteDataSource"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import javax.inject.Inject
class AIRepository @Inject constructor(
private val aiRemoteDataSource: AIRemoteDataSource
) {
suspend fun generateIngredients(imageData: String): String {
return aiRemoteDataSource.generateIngredients(imageData)
suspend fun generateIngredients(image: Bitmap): String {
return aiRemoteDataSource.generateIngredients(image)
}

suspend fun generateRecipe(ingredients: String, notes: String): RecipeSchema? {
Expand All @@ -28,4 +28,8 @@ class AIRepository @Inject constructor(
suspend fun scanMeal(imageData: String): MealSchema? {
return aiRemoteDataSource.scanMeal(imageData)
}

suspend fun loadOnDeviceModel() {
aiRemoteDataSource.loadOnDeviceModel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.google.firebase.example.friendlymeals.data.repository.AuthRepository
import com.google.firebase.example.friendlymeals.data.repository.DatabaseRepository
import com.google.firebase.example.friendlymeals.data.repository.StorageRepository
import com.google.firebase.example.friendlymeals.data.schema.RecipeSchema
import com.google.firebase.example.friendlymeals.ui.shared.toBase64
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -28,6 +27,7 @@ class GenerateViewModel @Inject constructor(

init {
loadCurrentUser()
loadOnDeviceModel()
}

fun loadCurrentUser() {
Expand All @@ -39,6 +39,12 @@ class GenerateViewModel @Inject constructor(
}
}

fun loadOnDeviceModel() {
launchCatching {
aiRepository.loadOnDeviceModel()
}
}

fun onIngredientsUpdated(ingredients: String) {
_viewState.value = _viewState.value.copy(
ingredients = ingredients
Expand All @@ -62,7 +68,7 @@ class GenerateViewModel @Inject constructor(
ingredientsLoading = true
)

val ingredients = aiRepository.generateIngredients(image.toBase64())
val ingredients = aiRepository.generateIngredients(image)

_viewState.value = _viewState.value.copy(
ingredientsLoading = false,
Expand Down
16 changes: 10 additions & 6 deletions app/src/main/res/xml/remote_config_defaults.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?><defaults>
<entry>
<key>generate_ingredients</key>
<value>generate-ingredients-template-v1-0-0</value>
<key>hybrid_cloud_model</key>
<value>gemini-3.1-flash-lite-preview</value>
</entry>
<entry>
<key>generate_recipe</key>
<value>generate-recipe-template-v1-0-0</value>
<key>hybrid_ingredients_prompt</key>
<value>Please analyze this image and list all visible food ingredients. Output ONLY a comma-separated list of ingredients. Do not include any introductory text, headers, or concluding remarks. Provide the raw list only. Be specific with measurements where possible.</value>
</entry>
<entry>
<key>scan_meal</key>
<value>scan-meal-template-v1-0-0</value>
</entry>
<entry>
<key>generate_recipe_photo_gemini</key>
Expand All @@ -16,7 +20,7 @@
<value>generate-recipe-photo-imagen-template-v1-0-0</value>
</entry>
<entry>
<key>scan_meal</key>
<value>scan-meal-template-v1-0-0</value>
<key>generate_recipe</key>
<value>generate-recipe-template-v1-0-0</value>
</entry>
</defaults>
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ plugins {
alias(libs.plugins.google.services) apply false
alias(libs.plugins.google.hilt) apply false
alias(libs.plugins.google.ksp) apply false
alias(libs.plugins.firebase.perf) apply false
}
10 changes: 8 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
agp = "8.13.1"
coilCompose = "2.7.0"
exifinterface = "1.4.2"
firebaseBom = "34.8.0"
firebaseAi = "17.10.1"
firebaseAiOndevice = "16.0.0-beta01"
firebaseBom = "34.11.0"
kotlin = "2.2.21"
coreKtx = "1.17.0"
junit = "4.13.2"
Expand All @@ -21,16 +23,19 @@ navigationCompose = "2.9.6"
constraintlayoutCompose = "1.1.1"
kotlinxSerializationJson = "1.9.0"
richtextCommonmark = "1.0.0-alpha03"
firebasePerf = "2.0.2"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
firebase-ai = { module = "com.google.firebase:firebase-ai", version.ref = "firebaseAi" }
firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version.ref = "firebaseAiOndevice" }
firebase-auth = { module = "com.google.firebase:firebase-auth" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-ai = { module = "com.google.firebase:firebase-ai" }
firebase-config = { module = "com.google.firebase:firebase-config" }
firebase-firestore = { module = "com.google.firebase:firebase-firestore" }
firebase-perf = { module = "com.google.firebase:firebase-perf" }
firebase-storage = { module = "com.google.firebase:firebase-storage" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand Down Expand Up @@ -62,4 +67,5 @@ google-services = { id = "com.google.gms.google-services", version.ref = "google
google-hilt = { id = "com.google.dagger.hilt.android", version.ref = "googleHilt" }
google-ksp = { id = "com.google.devtools.ksp", version.ref ="googleKotlinKsp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerf" }