From a388739b80e81d336ffdf4986367045729d31060 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 13 Oct 2025 08:58:57 -0400 Subject: [PATCH 01/14] [ALF] Add LiveAPI video sample --- .../quickstart/ai/FirebaseAISamples.kt | 32 +++++++++++++++++++ .../firebase/quickstart/ai/MainActivity.kt | 9 ++++++ .../feature/live/StreamRealtimeVideoScreen.kt | 17 ++++++++++ 3 files changed, 58 insertions(+) create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index ad3a5cd43..c98e4cfda 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt @@ -275,6 +275,38 @@ val FIREBASE_AI_SAMPLES = listOf( description = "Use bidirectional streaming to get information about" + " weather conditions for a specific US city on a specific date", navRoute = "stream", + modelName = "gemini-2.0-flash-live-preview-04-09", + //backend = GenerativeBackend.vertexAI(), + categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING), + tools = listOf( + Tool.functionDeclarations( + listOf( + FunctionDeclaration( + "fetchWeather", + "Get the weather conditions for a specific US city on a specific date.", + mapOf( + "city" to Schema.string("The US city of the location."), + "state" to Schema.string("The US state of the location."), + "date" to Schema.string( + "The date for which to get the weather." + + " Date must be in the format: YYYY-MM-DD." + ), + ), + ) + ) + ) + ), + initialPrompt = content { + text("What was the weather in Boston, MA on October 17, 2024?") + } + ), + Sample( + title = "Video input", + description = "Use bidirectional streaming to chat with Gemini using your" + + " phone's camera", + navRoute = "stream", + modelName = "gemini-2.0-flash-live-preview-04-09", + //backend = GenerativeBackend.vertexAI(), categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING), tools = listOf( Tool.functionDeclarations( 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 54eaff654..7a385f647 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 @@ -31,6 +31,8 @@ import androidx.navigation.compose.rememberNavController import com.google.firebase.ai.type.toImagenInlineImage import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeScreen +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoScreen import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenRoute import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenScreen import com.google.firebase.quickstart.ai.feature.text.ChatRoute @@ -90,6 +92,9 @@ class MainActivity : ComponentActivity() { "stream" -> { navController.navigate(StreamRealtimeRoute(it.id)) } + "streamVideo" -> { + navController.navigate(StreamRealtimeVideoRoute(it.id)) + } } } ) @@ -106,6 +111,10 @@ class MainActivity : ComponentActivity() { composable { StreamRealtimeScreen() } + + composable { + StreamRealtimeVideoScreen() + } } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt new file mode 100644 index 000000000..b6c752aca --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -0,0 +1,17 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.Manifest +import androidx.annotation.RequiresPermission +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel +import kotlinx.serialization.Serializable + +@Serializable +class StreamRealtimeVideoRoute(val sampleId: String) + +@RequiresPermission(Manifest.permission.RECORD_AUDIO) +@Composable +fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { + +} \ No newline at end of file From b797bedacc45583187e70f8e582deee26234efc9 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 13 Oct 2025 09:29:57 -0400 Subject: [PATCH 02/14] Actually runs this time --- .../quickstart/ai/FirebaseAISamples.kt | 8 +++--- .../firebase/quickstart/ai/MainActivity.kt | 2 +- .../ai/feature/live/BidiViewModel.kt | 2 +- .../feature/live/StreamRealtimeVideoScreen.kt | 27 ++++++++++++++++++- firebase-ai/gradle/libs.versions.toml | 2 +- firebase-ai/settings.gradle.kts | 1 + 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index c98e4cfda..649251cd8 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt @@ -275,8 +275,7 @@ val FIREBASE_AI_SAMPLES = listOf( description = "Use bidirectional streaming to get information about" + " weather conditions for a specific US city on a specific date", navRoute = "stream", - modelName = "gemini-2.0-flash-live-preview-04-09", - //backend = GenerativeBackend.vertexAI(), + modelName = "gemini-live-2.5-flash-preview", categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING), tools = listOf( Tool.functionDeclarations( @@ -305,9 +304,8 @@ val FIREBASE_AI_SAMPLES = listOf( description = "Use bidirectional streaming to chat with Gemini using your" + " phone's camera", navRoute = "stream", - modelName = "gemini-2.0-flash-live-preview-04-09", - //backend = GenerativeBackend.vertexAI(), - categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING), + modelName = "gemini-live-2.5-flash-preview", + categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING), tools = listOf( Tool.functionDeclarations( listOf( 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 7a385f647..57bab3aa7 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 @@ -111,7 +111,7 @@ class MainActivity : ComponentActivity() { composable { StreamRealtimeScreen() } - + // Stream Realtime Video Samples composable { StreamRealtimeVideoScreen() } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index a7f183704..79ef5c67e 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -65,7 +65,7 @@ class BidiViewModel( } @OptIn(PublicPreviewAPI::class) val liveModel = FirebaseAI.getInstance(Firebase.app, sample.backend).liveModel( - "gemini-live-2.5-flash", + modelName = sample.modelName ?: "gemini-live-2.5-flash", generationConfig = liveGenerationConfig, tools = sample.tools ) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index b6c752aca..b8f858ede 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -2,7 +2,17 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest import androidx.annotation.RequiresPermission +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +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.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel import kotlinx.serialization.Serializable @@ -13,5 +23,20 @@ class StreamRealtimeVideoRoute(val sampleId: String) @RequiresPermission(Manifest.permission.RECORD_AUDIO) @Composable fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { - + val backgroundColor = + MaterialTheme.colorScheme.background + Surface( + modifier = Modifier.fillMaxSize(), + color = backgroundColor + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("mytext") + } + } } \ No newline at end of file diff --git a/firebase-ai/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml index 49abf9fe7..b62c6195b 100644 --- a/firebase-ai/gradle/libs.versions.toml +++ b/firebase-ai/gradle/libs.versions.toml @@ -36,7 +36,7 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"} -firebase-ai = { module = "com.google.firebase:firebase-ai" } +firebase-ai = { module = "com.google.firebase:firebase-ai", version = "17.5.0" } 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" } diff --git a/firebase-ai/settings.gradle.kts b/firebase-ai/settings.gradle.kts index f1cb15d71..26668b936 100644 --- a/firebase-ai/settings.gradle.kts +++ b/firebase-ai/settings.gradle.kts @@ -14,6 +14,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() } From cb0930699c65647e12fa976cc5f07f27e018f503 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 13 Oct 2025 09:40:30 -0400 Subject: [PATCH 03/14] Point to the right screen --- .../java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt | 2 +- .../quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index 649251cd8..573f554dc 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt @@ -303,7 +303,7 @@ val FIREBASE_AI_SAMPLES = listOf( title = "Video input", description = "Use bidirectional streaming to chat with Gemini using your" + " phone's camera", - navRoute = "stream", + navRoute = "streamVideo", modelName = "gemini-live-2.5-flash-preview", categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING), tools = listOf( diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index b8f858ede..3cd123add 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel @@ -20,7 +21,6 @@ import kotlinx.serialization.Serializable @Serializable class StreamRealtimeVideoRoute(val sampleId: String) -@RequiresPermission(Manifest.permission.RECORD_AUDIO) @Composable fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { val backgroundColor = From a64723b053f8a93f5fc521c7a02296c1ddc55caa Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Wed, 15 Oct 2025 01:38:20 -0400 Subject: [PATCH 04/14] feat: Implement camera frame capture and logging - Modified StreamRealtimeVideoScreen to reduce the camera view to half the screen. - Implemented a frame analyzer in CameraView to capture frames once per second. - Added logging for the size of the captured frame's byte array. --- .../quickstart/ai/feature/live/CameraView.kt | 102 ++++++++++++++++++ .../feature/live/StreamRealtimeVideoScreen.kt | 24 ++--- 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt new file mode 100644 index 000000000..1f0839299 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -0,0 +1,102 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.annotation.SuppressLint +import android.content.Context +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import java.nio.ByteBuffer + +@Composable +fun CameraView( + modifier: Modifier = Modifier, + cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, + onFrameCaptured: (ByteArray) -> 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: (ByteArray) -> Unit +) { + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + val imageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(ContextCompat.getMainExecutor(previewView.context), SecondIntervalAnalyzer(onFrameCaptured)) + } + + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageAnalysis + ) +} + +private class SecondIntervalAnalyzer(private val onFrameCaptured: (ByteArray) -> Unit) : ImageAnalysis.Analyzer { + private var lastFrameTimestamp = 0L + private val interval = 1000L // 1 second + + @SuppressLint("UnsafeOptInUsageError") + override fun analyze(image: ImageProxy) { + val currentTimestamp = System.currentTimeMillis() + if (lastFrameTimestamp == 0L) { + lastFrameTimestamp = currentTimestamp + } + + if (currentTimestamp - lastFrameTimestamp >= interval) { + onFrameCaptured(image.toByteArray()) + lastFrameTimestamp = currentTimestamp + } + image.close() + } +} + +private fun ImageProxy.toByteArray(): ByteArray { + val buffer: ByteBuffer = planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + return bytes +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index 3cd123add..beda997fd 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -1,19 +1,15 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest +import android.util.Log import androidx.annotation.RequiresPermission -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding 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.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel import kotlinx.serialization.Serializable @@ -21,22 +17,26 @@ import kotlinx.serialization.Serializable @Serializable class StreamRealtimeVideoRoute(val sampleId: String) +@RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA]) @Composable fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { val backgroundColor = MaterialTheme.colorScheme.background + Surface( modifier = Modifier.fillMaxSize(), color = backgroundColor ) { - Column( + Box( modifier = Modifier .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center ) { - Text("mytext") + CameraView( + modifier = Modifier.fillMaxHeight(0.5f), + onFrameCaptured = { byteArray -> + Log.d("CameraFeed", "Captured frame size: ${byteArray.size}") + } + ) } } } \ No newline at end of file From d4a71ff109410fff64e5043084eba892a6c297cd Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Wed, 15 Oct 2025 01:44:57 -0400 Subject: [PATCH 05/14] Additional changes not captured by the previous commit --- firebase-ai/app/build.gradle.kts | 7 ++++ firebase-ai/app/src/main/AndroidManifest.xml | 6 ++- .../quickstart/ai/FirebaseAISamples.kt | 6 ++- .../firebase/quickstart/ai/MainActivity.kt | 4 ++ .../quickstart/ai/feature/live/CameraView.kt | 42 ++++++++++++++----- .../feature/live/StreamRealtimeVideoScreen.kt | 7 +++- firebase-ai/gradle/libs.versions.toml | 6 +++ 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/firebase-ai/app/build.gradle.kts b/firebase-ai/app/build.gradle.kts index f350bc2ad..6f88d35e5 100644 --- a/firebase-ai/app/build.gradle.kts +++ b/firebase-ai/app/build.gradle.kts @@ -67,6 +67,13 @@ dependencies { // Webkit implementation(libs.androidx.webkit) + // CameraX + 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) diff --git a/firebase-ai/app/src/main/AndroidManifest.xml b/firebase-ai/app/src/main/AndroidManifest.xml index 699c61714..268debbab 100644 --- a/firebase-ai/app/src/main/AndroidManifest.xml +++ b/firebase-ai/app/src/main/AndroidManifest.xml @@ -4,8 +4,12 @@ - + + + + + Unit + onFrameCaptured: (Bitmap) -> Unit ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -53,7 +58,7 @@ private fun bindPreview( previewView: PreviewView, cameraProvider: ProcessCameraProvider, cameraSelector: CameraSelector, - onFrameCaptured: (ByteArray) -> Unit + onFrameCaptured: (Bitmap) -> Unit ) { val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) @@ -75,7 +80,7 @@ private fun bindPreview( ) } -private class SecondIntervalAnalyzer(private val onFrameCaptured: (ByteArray) -> Unit) : ImageAnalysis.Analyzer { +private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : ImageAnalysis.Analyzer { private var lastFrameTimestamp = 0L private val interval = 1000L // 1 second @@ -87,16 +92,33 @@ private class SecondIntervalAnalyzer(private val onFrameCaptured: (ByteArray) -> } if (currentTimestamp - lastFrameTimestamp >= interval) { - onFrameCaptured(image.toByteArray()) + onFrameCaptured(image.toBitmap()) lastFrameTimestamp = currentTimestamp } image.close() } } -private fun ImageProxy.toByteArray(): ByteArray { - val buffer: ByteBuffer = planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) - return bytes +@SuppressLint("UnsafeOptInUsageError") +private fun ImageProxy.toBitmap(): Bitmap { + val yBuffer = planes[0].buffer // Y + val uBuffer = planes[1].buffer // U + val vBuffer = planes[2].buffer // V + + val ySize = yBuffer.remaining() + val uSize = uBuffer.remaining() + val vSize = vBuffer.remaining() + + val nv21 = ByteArray(ySize + uSize + vSize) + + //U and V are swapped + yBuffer.get(nv21, 0, ySize) + vBuffer.get(nv21, ySize, vSize) + uBuffer.get(nv21, ySize + vSize, uSize) + + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) + val out = ByteArrayOutputStream() + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 100, out) + val imageBytes = out.toByteArray() + return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index beda997fd..3ea84e780 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -1,6 +1,7 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest +import android.graphics.Bitmap import android.util.Log import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.Box @@ -13,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel import kotlinx.serialization.Serializable +import java.io.ByteArrayOutputStream @Serializable class StreamRealtimeVideoRoute(val sampleId: String) @@ -33,7 +35,10 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel ) { CameraView( modifier = Modifier.fillMaxHeight(0.5f), - onFrameCaptured = { byteArray -> + onFrameCaptured = { bitmap -> + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) + val byteArray = stream.toByteArray() Log.d("CameraFeed", "Captured frame size: ${byteArray.size}") } ) diff --git a/firebase-ai/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml index b62c6195b..01a6886c3 100644 --- a/firebase-ai/gradle/libs.versions.toml +++ b/firebase-ai/gradle/libs.versions.toml @@ -14,6 +14,7 @@ lifecycle = "2.9.4" lifecycleRuntimeKtx = "2.8.7" material = "1.13.0" webkit = "1.14.0" +camerax = "1.4.0-alpha05" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -41,6 +42,11 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 6c59bf42c62b64d96beab60d7d949d82301dfea7 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Wed, 15 Oct 2025 12:11:28 -0400 Subject: [PATCH 06/14] Capture audio --- .../ai/feature/live/BidiViewModel.kt | 47 ++++++++++--------- .../feature/live/StreamRealtimeVideoScreen.kt | 20 +++++--- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index 79ef5c67e..f0e694bc2 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -9,43 +9,28 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.google.firebase.Firebase import com.google.firebase.ai.FirebaseAI -import com.google.firebase.ai.ImagenModel -import com.google.firebase.ai.LiveGenerativeModel -import com.google.firebase.ai.ai import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart -import com.google.firebase.ai.type.GenerativeBackend -import com.google.firebase.ai.type.ImagenAspectRatio -import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.ImagenPersonFilterLevel -import com.google.firebase.ai.type.ImagenSafetyFilterLevel -import com.google.firebase.ai.type.ImagenSafetySettings import com.google.firebase.ai.type.InlineDataPart -import com.google.firebase.ai.type.LiveServerContent -import com.google.firebase.ai.type.LiveServerMessage import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseModality import com.google.firebase.ai.type.SpeechConfig -import com.google.firebase.ai.type.TextPart -import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.Voice -import com.google.firebase.ai.type.asTextOrNull -import com.google.firebase.ai.type.imagenGenerationConfig import com.google.firebase.ai.type.liveGenerationConfig import com.google.firebase.app import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute -import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive +import java.io.ByteArrayOutputStream +import kotlin.time.Duration.Companion.seconds @OptIn(PublicPreviewAPI::class) class BidiViewModel( @@ -63,6 +48,7 @@ class BidiViewModel( // Change this to ContentModality.TEXT if you want text output. responseModality = ResponseModality.AUDIO } + @OptIn(PublicPreviewAPI::class) val liveModel = FirebaseAI.getInstance(Firebase.app, sample.backend).liveModel( modelName = sample.modelName ?: "gemini-live-2.5-flash", @@ -74,30 +60,45 @@ class BidiViewModel( } } - fun handler(fetchWeatherCall: FunctionCallPart) : FunctionResponsePart { - val response:JsonObject + fun handler(fetchWeatherCall: FunctionCallPart): FunctionResponsePart { + val response: JsonObject fetchWeatherCall.let { val city = it.args["city"]?.jsonPrimitive?.content val state = it.args["state"]?.jsonPrimitive?.content val date = it.args["date"]?.jsonPrimitive?.content runBlocking { - response = if(!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { + response = if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { fetchWeather(city!!, state!!, date!!) } else { JsonObject(emptyMap()) } } } - return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) + return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } + @RequiresPermission(Manifest.permission.RECORD_AUDIO) suspend fun startConversation() { - liveSession.startAudioConversation(::handler) + liveSession.startAudioConversation(::handler) } fun endConversation() { liveSession.stopAudioConversation() } +// ... (imports and class definition) + + @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) + 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( + InlineDataPart(jpegBytes, "image/jpeg") + ) + } + } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index 3ea84e780..abcf54c1a 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -2,7 +2,6 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest import android.graphics.Bitmap -import android.util.Log import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight @@ -10,11 +9,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import java.io.ByteArrayOutputStream @Serializable class StreamRealtimeVideoRoute(val sampleId: String) @@ -25,6 +26,16 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel val backgroundColor = MaterialTheme.colorScheme.background + val scope = rememberCoroutineScope() + DisposableEffect(Unit) { + scope.launch { + bidiView.startConversation() + } + onDispose { + bidiView.endConversation() + } + } + Surface( modifier = Modifier.fillMaxSize(), color = backgroundColor @@ -36,10 +47,7 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel CameraView( modifier = Modifier.fillMaxHeight(0.5f), onFrameCaptured = { bitmap -> - val stream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) - val byteArray = stream.toByteArray() - Log.d("CameraFeed", "Captured frame size: ${byteArray.size}") + bidiView.sendVideoFrame(bitmap) } ) } From 1d0a1cae7acb11d96d7c4c5226ae2f54cb53f091 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 02:36:04 -0400 Subject: [PATCH 07/14] Reorg code --- .../ai/feature/live/BidiViewModel.kt | 8 ++---- .../quickstart/ai/feature/live/CameraView.kt | 26 +------------------ .../feature/live/StreamRealtimeVideoScreen.kt | 1 - 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index f0e694bc2..96c405719 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -11,7 +11,7 @@ import com.google.firebase.Firebase import com.google.firebase.ai.FirebaseAI import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart -import com.google.firebase.ai.type.InlineDataPart +import com.google.firebase.ai.type.InlineData import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseModality @@ -22,15 +22,11 @@ import com.google.firebase.app import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive import java.io.ByteArrayOutputStream -import kotlin.time.Duration.Companion.seconds @OptIn(PublicPreviewAPI::class) class BidiViewModel( @@ -97,7 +93,7 @@ class BidiViewModel( val jpegBytes = byteArrayOutputStream.toByteArray() liveSession.sendVideoRealtime( - InlineDataPart(jpegBytes, "image/jpeg") + InlineData(jpegBytes, "image/jpeg") ) } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt index e019e4d72..6a520daa7 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -97,28 +97,4 @@ private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Un } image.close() } -} - -@SuppressLint("UnsafeOptInUsageError") -private fun ImageProxy.toBitmap(): Bitmap { - val yBuffer = planes[0].buffer // Y - val uBuffer = planes[1].buffer // U - val vBuffer = planes[2].buffer // V - - val ySize = yBuffer.remaining() - val uSize = uBuffer.remaining() - val vSize = vBuffer.remaining() - - val nv21 = ByteArray(ySize + uSize + vSize) - - //U and V are swapped - yBuffer.get(nv21, 0, ySize) - vBuffer.get(nv21, ySize, vSize) - uBuffer.get(nv21, ySize + vSize, uSize) - - val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) - val out = ByteArrayOutputStream() - yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 100, out) - val imageBytes = out.toByteArray() - return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) -} +} \ No newline at end of file diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index abcf54c1a..377206a09 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -1,7 +1,6 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest -import android.graphics.Bitmap import androidx.annotation.RequiresPermission import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight From dba04b3143a433351bec0658869a960a54f967d9 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 02:52:33 -0400 Subject: [PATCH 08/14] Permissions correctly requested --- .../firebase/quickstart/ai/MainActivity.kt | 9 +-- .../ai/feature/live/BidiViewModel.kt | 8 +- .../ai/feature/live/StreamRealtimeScreen.kt | 2 - .../feature/live/StreamRealtimeVideoScreen.kt | 78 +++++++++++++++---- 4 files changed, 69 insertions(+), 28 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index 2b6cff581..e9cfb657e 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 @@ -44,14 +44,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if(ContextCompat.checkSelfPermission(this, - Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } - if(ContextCompat.checkSelfPermission(this, - Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1) - } + enableEdgeToEdge() catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat) setContent { diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index 96c405719..9468ae0cd 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -1,4 +1,4 @@ -package com.google.firebase.quickstart.ai.feature.media.imagen +package com.google.firebase.quickstart.ai.feature.live import android.Manifest import android.graphics.Bitmap @@ -73,7 +73,7 @@ class BidiViewModel( return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } - @RequiresPermission(Manifest.permission.RECORD_AUDIO) + suspend fun startConversation() { liveSession.startAudioConversation(::handler) } @@ -82,9 +82,7 @@ class BidiViewModel( liveSession.stopAudioConversation() } -// ... (imports and class definition) - - @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) + fun sendVideoFrame(frame: Bitmap) { viewModelScope.launch { // Directly compress the Bitmap to a ByteArray diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt index f088cda23..194b04023 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt @@ -32,8 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index 377206a09..1702b6804 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -1,18 +1,31 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest +import android.content.Context +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.media.imagen.BidiViewModel +import com.google.firebase.quickstart.ai.feature.live.BidiViewModel import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -26,9 +39,42 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel MaterialTheme.colorScheme.background val scope = rememberCoroutineScope() - DisposableEffect(Unit) { - scope.launch { - bidiView.startConversation() + + 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() @@ -39,16 +85,22 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel modifier = Modifier.fillMaxSize(), color = backgroundColor ) { - Box( - modifier = Modifier - .fillMaxSize() - ) { - CameraView( - modifier = Modifier.fillMaxHeight(0.5f), - onFrameCaptured = { bitmap -> - bidiView.sendVideoFrame(bitmap) + 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.") + } } } } \ No newline at end of file From af5b172dabd4e5235280e77a21963d6cd11e056d Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 12:08:40 -0400 Subject: [PATCH 09/14] Format new files using ktfmt --- .../ai/feature/live/BidiViewModel.kt | 42 +++++------- .../quickstart/ai/feature/live/CameraView.kt | 66 +++++++++---------- .../feature/live/StreamRealtimeVideoScreen.kt | 60 +++++------------ 3 files changed, 66 insertions(+), 102 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index 9468ae0cd..73bd703d1 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -1,8 +1,6 @@ package com.google.firebase.quickstart.ai.feature.live -import android.Manifest import android.graphics.Bitmap -import androidx.annotation.RequiresPermission import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -20,18 +18,15 @@ import com.google.firebase.ai.type.Voice import com.google.firebase.ai.type.liveGenerationConfig import com.google.firebase.app import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES -import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather +import java.io.ByteArrayOutputStream import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive -import java.io.ByteArrayOutputStream @OptIn(PublicPreviewAPI::class) -class BidiViewModel( - savedStateHandle: SavedStateHandle -) : ViewModel() { +class BidiViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val sampleId = savedStateHandle.toRoute().sampleId private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId } @@ -46,14 +41,14 @@ class BidiViewModel( } @OptIn(PublicPreviewAPI::class) - val liveModel = FirebaseAI.getInstance(Firebase.app, sample.backend).liveModel( - modelName = sample.modelName ?: "gemini-live-2.5-flash", - generationConfig = liveGenerationConfig, - tools = sample.tools - ) - runBlocking { - liveSession = liveModel.connect() - } + val liveModel = + FirebaseAI.getInstance(Firebase.app, sample.backend) + .liveModel( + modelName = sample.modelName ?: "gemini-live-2.5-flash", + generationConfig = liveGenerationConfig, + tools = sample.tools, + ) + runBlocking { liveSession = liveModel.connect() } } fun handler(fetchWeatherCall: FunctionCallPart): FunctionResponsePart { @@ -63,17 +58,17 @@ class BidiViewModel( val state = it.args["state"]?.jsonPrimitive?.content val date = it.args["date"]?.jsonPrimitive?.content runBlocking { - response = if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { - fetchWeather(city!!, state!!, date!!) - } else { - JsonObject(emptyMap()) - } + response = + if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { + fetchWeather(city!!, state!!, date!!) + } else { + JsonObject(emptyMap()) + } } } return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } - suspend fun startConversation() { liveSession.startAudioConversation(::handler) } @@ -82,7 +77,6 @@ class BidiViewModel( liveSession.stopAudioConversation() } - fun sendVideoFrame(frame: Bitmap) { viewModelScope.launch { // Directly compress the Bitmap to a ByteArray @@ -90,9 +84,7 @@ class BidiViewModel( frame.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream) val jpegBytes = byteArrayOutputStream.toByteArray() - liveSession.sendVideoRealtime( - InlineData(jpegBytes, "image/jpeg") - ) + liveSession.sendVideoRealtime(InlineData(jpegBytes, "image/jpeg")) } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt index 6a520daa7..f29136ec3 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -1,12 +1,7 @@ package com.google.firebase.quickstart.ai.feature.live import android.annotation.SuppressLint -import android.content.Context import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.ImageFormat -import android.graphics.Rect -import android.graphics.YuvImage import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy @@ -21,13 +16,12 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner -import java.io.ByteArrayOutputStream @Composable fun CameraView( modifier: Modifier = Modifier, cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, - onFrameCaptured: (Bitmap) -> Unit + onFrameCaptured: (Bitmap) -> Unit, ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -37,16 +31,19 @@ fun CameraView( factory = { ctx -> val previewView = PreviewView(ctx) val executor = ContextCompat.getMainExecutor(ctx) - cameraProviderFuture.addListener({ - val cameraProvider = cameraProviderFuture.get() - bindPreview( - lifecycleOwner, - previewView, - cameraProvider, - cameraSelector, - onFrameCaptured - ) - }, executor) + cameraProviderFuture.addListener( + { + val cameraProvider = cameraProviderFuture.get() + bindPreview( + lifecycleOwner, + previewView, + cameraProvider, + cameraSelector, + onFrameCaptured, + ) + }, + executor, + ) previewView }, modifier = modifier, @@ -58,29 +55,28 @@ private fun bindPreview( previewView: PreviewView, cameraProvider: ProcessCameraProvider, cameraSelector: CameraSelector, - onFrameCaptured: (Bitmap) -> Unit + onFrameCaptured: (Bitmap) -> Unit, ) { - val preview = Preview.Builder().build().also { - it.setSurfaceProvider(previewView.surfaceProvider) - } + val preview = + Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } - val imageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - .also { - it.setAnalyzer(ContextCompat.getMainExecutor(previewView.context), SecondIntervalAnalyzer(onFrameCaptured)) - } + val imageAnalysis = + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer( + ContextCompat.getMainExecutor(previewView.context), + SecondIntervalAnalyzer(onFrameCaptured), + ) + } cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - lifecycleOwner, - cameraSelector, - preview, - imageAnalysis - ) + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) } -private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : ImageAnalysis.Analyzer { +private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : + ImageAnalysis.Analyzer { private var lastFrameTimestamp = 0L private val interval = 1000L // 1 second @@ -97,4 +93,4 @@ private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Un } image.close() } -} \ No newline at end of file +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt index 1702b6804..a30c93980 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -1,7 +1,6 @@ package com.google.firebase.quickstart.ai.feature.live import android.Manifest -import android.content.Context import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -25,77 +24,54 @@ 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 import kotlinx.serialization.Serializable -@Serializable -class StreamRealtimeVideoRoute(val sampleId: String) +@Serializable class StreamRealtimeVideoRoute(val sampleId: String) @RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA]) @Composable fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { - val backgroundColor = - MaterialTheme.colorScheme.background + val 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 + 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 } - } + 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 - ) - ) + launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) } } DisposableEffect(hasPermissions) { if (hasPermissions) { - scope.launch { - bidiView.startConversation() - } - } - onDispose { - bidiView.endConversation() + scope.launch { bidiView.startConversation() } } + onDispose { bidiView.endConversation() } } - Surface( - modifier = Modifier.fillMaxSize(), - color = backgroundColor - ) { + Surface(modifier = Modifier.fillMaxSize(), color = backgroundColor) { Column(modifier = Modifier.fillMaxSize()) { if (hasPermissions) { - Box( - modifier = Modifier - .fillMaxSize() - ) { + Box(modifier = Modifier.fillMaxSize()) { CameraView( modifier = Modifier.fillMaxHeight(0.5f), - onFrameCaptured = { bitmap -> - bidiView.sendVideoFrame(bitmap) - } + onFrameCaptured = { bitmap -> bidiView.sendVideoFrame(bitmap) }, ) } } else { @@ -103,4 +79,4 @@ fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel } } } -} \ No newline at end of file +} From 1fae483da8da3fcea884f6e85e4ceaac8be95f9a Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 12:20:49 -0400 Subject: [PATCH 10/14] Better naming --- .../firebase/quickstart/ai/feature/live/CameraView.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt index f29136ec3..8fcf0258b 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner +import kotlin.time.Duration.Companion.seconds @Composable fun CameraView( @@ -67,7 +68,7 @@ private fun bindPreview( .also { it.setAnalyzer( ContextCompat.getMainExecutor(previewView.context), - SecondIntervalAnalyzer(onFrameCaptured), + SnapshotFrameAnalyzer(onFrameCaptured), ) } @@ -75,10 +76,11 @@ private fun bindPreview( cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) } -private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : +// 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 = 1000L // 1 second + private val interval = 1.seconds // 1 second @SuppressLint("UnsafeOptInUsageError") override fun analyze(image: ImageProxy) { @@ -87,7 +89,7 @@ private class SecondIntervalAnalyzer(private val onFrameCaptured: (Bitmap) -> Un lastFrameTimestamp = currentTimestamp } - if (currentTimestamp - lastFrameTimestamp >= interval) { + if (currentTimestamp - lastFrameTimestamp >= interval.inWholeMilliseconds) { onFrameCaptured(image.toBitmap()) lastFrameTimestamp = currentTimestamp } From a613fda425ae2dbb3753de2a15875133531d8861 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 18:19:29 -0400 Subject: [PATCH 11/14] Suppress unnecessary lints --- .../com/google/firebase/quickstart/ai/MainActivity.kt | 9 +++++++-- .../firebase/quickstart/ai/feature/live/BidiViewModel.kt | 3 +++ firebase-ai/app/src/main/res/values/colors.xml | 7 ------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index e9cfb657e..39e6e34e7 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -1,6 +1,7 @@ package com.google.firebase.quickstart.ai import android.Manifest +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -104,11 +105,15 @@ class MainActivity : ComponentActivity() { composable { ImagenScreen() } - // Stream Realtime Samples + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeScreen composable. + @SuppressLint("MissingPermission") composable { StreamRealtimeScreen() } - // Stream Realtime Video Samples + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeVideoScreen composable. + @SuppressLint("MissingPermission") composable { StreamRealtimeVideoScreen() } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index 73bd703d1..3f36b81c3 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -1,5 +1,6 @@ package com.google.firebase.quickstart.ai.feature.live +import android.annotation.SuppressLint import android.graphics.Bitmap import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -69,6 +70,8 @@ class BidiViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } + // The permission check is handled by the view that calls this function. + @SuppressLint("MissingPermission") suspend fun startConversation() { liveSession.startAudioConversation(::handler) } diff --git a/firebase-ai/app/src/main/res/values/colors.xml b/firebase-ai/app/src/main/res/values/colors.xml index f8c6127d3..55344e519 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 From ea0577a8ba07da7e337cd2d0c0b43edca93635f6 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 18:22:13 -0400 Subject: [PATCH 12/14] bump versions --- firebase-ai/gradle/libs.versions.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-ai/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml index 01a6886c3..3c78a5e2e 100644 --- a/firebase-ai/gradle/libs.versions.toml +++ b/firebase-ai/gradle/libs.versions.toml @@ -1,20 +1,20 @@ [versions] activityCompose = "1.11.0" -agp = "8.9.2" -composeBom = "2024.09.00" +agp = "8.13.0" +composeBom = "2025.10.00" composeNavigation = "2.9.5" coreKtx = "1.17.0" espressoCore = "3.7.0" firebaseBom = "34.4.0" junit = "4.13.2" junitVersion = "1.3.0" -kotlin = "2.0.21" +kotlin = "2.2.20" kotlinxSerializationCore = "1.9.0" lifecycle = "2.9.4" -lifecycleRuntimeKtx = "2.8.7" +lifecycleRuntimeKtx = "2.9.4" material = "1.13.0" webkit = "1.14.0" -camerax = "1.4.0-alpha05" +camerax = "1.5.1" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } From 904fcd936b1f32a6e0cc87dc64d16f378f1f3214 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 18:26:21 -0400 Subject: [PATCH 13/14] additional lint fixes --- firebase-ai/app/src/main/AndroidManifest.xml | 2 +- .../google/firebase/quickstart/ai/feature/text/ChatScreen.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase-ai/app/src/main/AndroidManifest.xml b/firebase-ai/app/src/main/AndroidManifest.xml index 268debbab..93521d7c3 100644 --- a/firebase-ai/app/src/main/AndroidManifest.xml +++ b/firebase-ai/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt index 276024c27..f89bcba17 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.Bitmap import android.net.Uri +import androidx.core.net.toUri import android.provider.OpenableColumns import android.text.format.Formatter import android.webkit.WebResourceRequest @@ -374,7 +375,7 @@ fun SourceLinkView( ClickableText(text = annotatedString, onClick = { offset -> annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))) + context.startActivity(Intent(Intent.ACTION_VIEW, annotation.item.toUri())) } }) } From 1db6a38c2c2c4a426f7de48939551f79ab8db640 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Thu, 16 Oct 2025 18:40:24 -0400 Subject: [PATCH 14/14] Add entries to top level libs.versions.toml --- gradle/libs.versions.toml | 46 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec8d0947a..4eb6420c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,49 +1,55 @@ [versions] +activityCompose = "1.11.0" agp = "8.13.0" +camerax = "1.5.1" coilCompose = "2.7.0" -firebaseBom = "34.4.0" -kotlin = "2.2.20" +composeBom = "2025.10.00" +composeNavigation = "2.9.5" coreKtx = "1.17.0" +espressoCore = "3.7.0" +firebaseBom = "34.4.0" +googleServices = "4.4.4" junit = "4.13.2" junitVersion = "1.3.0" -espressoCore = "3.7.0" +kotlin = "2.2.20" kotlinxSerializationCore = "1.9.0" lifecycle = "2.9.4" -activityCompose = "1.11.0" -composeBom = "2025.10.00" -googleServices = "4.4.4" -composeNavigation = "2.9.5" material = "1.13.0" webkit = "1.14.0" [libraries] +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-lifecycle-runtime-compose-android = { module = "androidx.lifecycle:lifecycle-runtime-compose-android", version.ref = "lifecycle" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } -coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } -firebase-ai = { module = "com.google.firebase:firebase-ai" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"} +firebase-ai = { module = "com.google.firebase:firebase-ai" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } +junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } -androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } material = { module = "com.google.android.material:material", version.ref = "material" } [plugins]