diff --git a/firebase-ai/app/build.gradle.kts b/firebase-ai/app/build.gradle.kts index f350bc2ad4..6f88d35e50 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 699c61714c..93521d7c3d 100644 --- a/firebase-ai/app/src/main/AndroidManifest.xml +++ b/firebase-ai/app/src/main/AndroidManifest.xml @@ -4,8 +4,12 @@ - + + + + + diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/FirebaseAISamples.kt index ad3a5cd435..53a06dd8cc 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,8 @@ 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", + backend = GenerativeBackend.vertexAI(), + modelName = "gemini-2.0-flash-live-preview-04-09", categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING), tools = listOf( Tool.functionDeclarations( @@ -298,6 +300,36 @@ val FIREBASE_AI_SAMPLES = listOf( 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 = "streamVideo", + backend = GenerativeBackend.vertexAI(), + modelName = "gemini-2.0-flash-live-preview-04-09", + categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING), + tools = listOf( + Tool.functionDeclarations( + listOf( + FunctionDeclaration( + "fetchWeather", + "Get the weather conditions for a specific US city on a specific date.", + mapOf( + "city" to Schema.string("The US city of the location."), + "state" to Schema.string("The US state of the location."), + "date" to Schema.string( + "The date for which to get the weather." + + " Date must be in the format: YYYY-MM-DD." + ), + ), + ) + ) + ) + ), + initialPrompt = content { + text("What was the weather in Boston, MA on October 17, 2024?") + } + ), Sample( title = "Weather Chat", description = "Use function calling to get the weather conditions" + diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index 54eaff6543..39e6e34e7a 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -1,6 +1,7 @@ package com.google.firebase.quickstart.ai import android.Manifest +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -31,6 +32,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 @@ -42,10 +45,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if(ContextCompat.checkSelfPermission(this, - Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1) - } + enableEdgeToEdge() catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat) setContent { @@ -90,6 +90,9 @@ class MainActivity : ComponentActivity() { "stream" -> { navController.navigate(StreamRealtimeRoute(it.id)) } + "streamVideo" -> { + navController.navigate(StreamRealtimeVideoRoute(it.id)) + } } } ) @@ -102,10 +105,18 @@ class MainActivity : ComponentActivity() { composable { ImagenScreen() } - // Stream Realtime Samples + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeScreen composable. + @SuppressLint("MissingPermission") composable { StreamRealtimeScreen() } + // The permission is checked by the @RequiresPermission annotation on the + // StreamRealtimeVideoScreen composable. + @SuppressLint("MissingPermission") + composable { + StreamRealtimeVideoScreen() + } } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt index a7f183704f..3f36b81c3f 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt @@ -1,56 +1,33 @@ -package com.google.firebase.quickstart.ai.feature.media.imagen +package com.google.firebase.quickstart.ai.feature.live -import android.Manifest +import android.annotation.SuppressLint import android.graphics.Bitmap -import androidx.annotation.RequiresPermission import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.google.firebase.Firebase import com.google.firebase.ai.FirebaseAI -import com.google.firebase.ai.ImagenModel -import com.google.firebase.ai.LiveGenerativeModel -import com.google.firebase.ai.ai import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart -import com.google.firebase.ai.type.GenerativeBackend -import com.google.firebase.ai.type.ImagenAspectRatio -import com.google.firebase.ai.type.ImagenImageFormat -import com.google.firebase.ai.type.ImagenPersonFilterLevel -import com.google.firebase.ai.type.ImagenSafetyFilterLevel -import com.google.firebase.ai.type.ImagenSafetySettings -import com.google.firebase.ai.type.InlineDataPart -import com.google.firebase.ai.type.LiveServerContent -import com.google.firebase.ai.type.LiveServerMessage +import com.google.firebase.ai.type.InlineData import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseModality import com.google.firebase.ai.type.SpeechConfig -import com.google.firebase.ai.type.TextPart -import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.Voice -import com.google.firebase.ai.type.asTextOrNull -import com.google.firebase.ai.type.imagenGenerationConfig import com.google.firebase.ai.type.liveGenerationConfig import com.google.firebase.app import com.google.firebase.quickstart.ai.FIREBASE_AI_SAMPLES -import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeRoute -import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import java.io.ByteArrayOutputStream import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive @OptIn(PublicPreviewAPI::class) -class BidiViewModel( - savedStateHandle: SavedStateHandle -) : ViewModel() { +class BidiViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val sampleId = savedStateHandle.toRoute().sampleId private val sample = FIREBASE_AI_SAMPLES.first { it.id == sampleId } @@ -63,41 +40,54 @@ class BidiViewModel( // Change this to ContentModality.TEXT if you want text output. responseModality = ResponseModality.AUDIO } + @OptIn(PublicPreviewAPI::class) - val liveModel = FirebaseAI.getInstance(Firebase.app, sample.backend).liveModel( - "gemini-live-2.5-flash", - generationConfig = liveGenerationConfig, - tools = sample.tools - ) - runBlocking { - liveSession = liveModel.connect() - } + val liveModel = + FirebaseAI.getInstance(Firebase.app, sample.backend) + .liveModel( + modelName = sample.modelName ?: "gemini-live-2.5-flash", + generationConfig = liveGenerationConfig, + tools = sample.tools, + ) + runBlocking { liveSession = liveModel.connect() } } - fun handler(fetchWeatherCall: FunctionCallPart) : FunctionResponsePart { - val response:JsonObject + fun handler(fetchWeatherCall: FunctionCallPart): FunctionResponsePart { + val response: JsonObject fetchWeatherCall.let { val city = it.args["city"]?.jsonPrimitive?.content val state = it.args["state"]?.jsonPrimitive?.content val date = it.args["date"]?.jsonPrimitive?.content runBlocking { - response = if(!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { - fetchWeather(city!!, state!!, date!!) - } else { - JsonObject(emptyMap()) - } + response = + if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and date.isNullOrEmpty()) { + fetchWeather(city!!, state!!, date!!) + } else { + JsonObject(emptyMap()) + } } } - return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) + return FunctionResponsePart("fetchWeather", response, fetchWeatherCall.id) } - @RequiresPermission(Manifest.permission.RECORD_AUDIO) + + // The permission check is handled by the view that calls this function. + @SuppressLint("MissingPermission") suspend fun startConversation() { - liveSession.startAudioConversation(::handler) + liveSession.startAudioConversation(::handler) } fun endConversation() { liveSession.stopAudioConversation() } + fun sendVideoFrame(frame: Bitmap) { + viewModelScope.launch { + // Directly compress the Bitmap to a ByteArray + val byteArrayOutputStream = ByteArrayOutputStream() + frame.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream) + val jpegBytes = byteArrayOutputStream.toByteArray() + liveSession.sendVideoRealtime(InlineData(jpegBytes, "image/jpeg")) + } + } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt new file mode 100644 index 0000000000..8fcf0258ba --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/CameraView.kt @@ -0,0 +1,98 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import kotlin.time.Duration.Companion.seconds + +@Composable +fun CameraView( + modifier: Modifier = Modifier, + cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, + onFrameCaptured: (Bitmap) -> Unit, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + + AndroidView( + factory = { ctx -> + val previewView = PreviewView(ctx) + val executor = ContextCompat.getMainExecutor(ctx) + cameraProviderFuture.addListener( + { + val cameraProvider = cameraProviderFuture.get() + bindPreview( + lifecycleOwner, + previewView, + cameraProvider, + cameraSelector, + onFrameCaptured, + ) + }, + executor, + ) + previewView + }, + modifier = modifier, + ) +} + +private fun bindPreview( + lifecycleOwner: LifecycleOwner, + previewView: PreviewView, + cameraProvider: ProcessCameraProvider, + cameraSelector: CameraSelector, + onFrameCaptured: (Bitmap) -> Unit, +) { + val preview = + Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } + + val imageAnalysis = + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer( + ContextCompat.getMainExecutor(previewView.context), + SnapshotFrameAnalyzer(onFrameCaptured), + ) + } + + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) +} + +// Calls the [onFrameCaptured] callback with the captured frame every second. +private class SnapshotFrameAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) : + ImageAnalysis.Analyzer { + private var lastFrameTimestamp = 0L + private val interval = 1.seconds // 1 second + + @SuppressLint("UnsafeOptInUsageError") + override fun analyze(image: ImageProxy) { + val currentTimestamp = System.currentTimeMillis() + if (lastFrameTimestamp == 0L) { + lastFrameTimestamp = currentTimestamp + } + + if (currentTimestamp - lastFrameTimestamp >= interval.inWholeMilliseconds) { + onFrameCaptured(image.toBitmap()) + lastFrameTimestamp = currentTimestamp + } + image.close() + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt index f088cda23b..194b04023f 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeScreen.kt @@ -32,8 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.BidiViewModel -import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt new file mode 100644 index 0000000000..a30c93980c --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamRealtimeVideoScreen.kt @@ -0,0 +1,82 @@ +package com.google.firebase.quickstart.ai.feature.live + +import android.Manifest +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresPermission +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable class StreamRealtimeVideoRoute(val sampleId: String) + +@RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA]) +@Composable +fun StreamRealtimeVideoScreen(bidiView: BidiViewModel = viewModel()) { + val backgroundColor = MaterialTheme.colorScheme.background + + val scope = rememberCoroutineScope() + + val context = LocalContext.current + var hasPermissions by remember { + mutableStateOf( + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == + PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + ) + } + + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + permissions -> + hasPermissions = permissions.values.all { it } + } + + LaunchedEffect(Unit) { + if (!hasPermissions) { + launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) + } + } + + DisposableEffect(hasPermissions) { + if (hasPermissions) { + scope.launch { bidiView.startConversation() } + } + onDispose { bidiView.endConversation() } + } + + Surface(modifier = Modifier.fillMaxSize(), color = backgroundColor) { + Column(modifier = Modifier.fillMaxSize()) { + if (hasPermissions) { + Box(modifier = Modifier.fillMaxSize()) { + CameraView( + modifier = Modifier.fillMaxHeight(0.5f), + onFrameCaptured = { bitmap -> bidiView.sendVideoFrame(bitmap) }, + ) + } + } else { + Text("Camera and audio permissions are required to use this feature.") + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt index 276024c27f..f89bcba17a 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatScreen.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Intent import android.graphics.Bitmap import android.net.Uri +import androidx.core.net.toUri import android.provider.OpenableColumns import android.text.format.Formatter import android.webkit.WebResourceRequest @@ -374,7 +375,7 @@ fun SourceLinkView( ClickableText(text = annotatedString, onClick = { offset -> annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))) + context.startActivity(Intent(Intent.ACTION_VIEW, annotation.item.toUri())) } }) } diff --git a/firebase-ai/app/src/main/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/gradle/libs.versions.toml b/firebase-ai/gradle/libs.versions.toml index 49abf9fe73..431f132872 100644 --- a/firebase-ai/gradle/libs.versions.toml +++ b/firebase-ai/gradle/libs.versions.toml @@ -1,19 +1,20 @@ [versions] activityCompose = "1.11.0" -agp = "8.9.2" -composeBom = "2024.09.00" +agp = "8.13.0" +composeBom = "2025.10.00" composeNavigation = "2.9.5" coreKtx = "1.17.0" espressoCore = "3.7.0" -firebaseBom = "34.4.0" +firebaseBom = "34.5.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.5.1" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -41,6 +42,11 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax" } +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/firebase-ai/settings.gradle.kts b/firebase-ai/settings.gradle.kts index f1cb15d710..26668b9361 100644 --- a/firebase-ai/settings.gradle.kts +++ b/firebase-ai/settings.gradle.kts @@ -14,6 +14,7 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c60c47da91..79096c7fa2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,17 @@ [versions] +activityCompose = "1.11.0" agp = "8.13.0" +camerax = "1.5.1" coilCompose = "2.7.0" -firebaseBom = "34.4.0" +firebaseBom = "34.5.0" kotlin = "2.2.21" 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" @@ -17,33 +22,38 @@ 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]