From ffa6e2d35bfa7eb4227b25806d0974d26fa8cac7 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:19:29 -0400 Subject: [PATCH] add structured output test --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- auth/gradle/wrapper/gradle-wrapper.properties | 2 +- build.gradle.kts | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- firebase-ai/app/build.gradle.kts | 27 +- .../firebase/quickstart/ai/MainActivity.kt | 109 ++++---- .../ai/feature/structured/MovieReview.kt | 20 ++ .../structured/StructuredOutputViewModel.kt | 196 +++++++++++++++ .../ai/ui/StructuredOutputScreen.kt | 234 ++++++++++++++++++ .../ai/ui/StructuredOutputUiState.kt | 12 + .../ai/ui/navigation/FirebaseAISamples.kt | 10 + .../quickstart/ai/ui/navigation/Sample.kt | 3 +- firebase-ai/build.gradle.kts | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/libs.versions.toml | 44 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- perf/gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 27 files changed, 610 insertions(+), 85 deletions(-) create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/MovieReview.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/StructuredOutputViewModel.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputScreen.kt create mode 100644 firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputUiState.kt diff --git a/admob/gradle/wrapper/gradle-wrapper.properties b/admob/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/admob/gradle/wrapper/gradle-wrapper.properties +++ b/admob/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/analytics/gradle/wrapper/gradle-wrapper.properties b/analytics/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/analytics/gradle/wrapper/gradle-wrapper.properties +++ b/analytics/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/appdistribution/gradle/wrapper/gradle-wrapper.properties b/appdistribution/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/appdistribution/gradle/wrapper/gradle-wrapper.properties +++ b/appdistribution/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/auth/gradle/wrapper/gradle-wrapper.properties b/auth/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/auth/gradle/wrapper/gradle-wrapper.properties +++ b/auth/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/build.gradle.kts b/build.gradle.kts index d60dd6bf8e..04520d1655 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,13 +8,15 @@ plugins { alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.navigation.safeargs) apply false alias(libs.plugins.gradle.versions) apply true + alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.ksp) apply false } allprojects { repositories { - google() mavenLocal() + google() mavenCentral() } } diff --git a/config/gradle/wrapper/gradle-wrapper.properties b/config/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/config/gradle/wrapper/gradle-wrapper.properties +++ b/config/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/crash/gradle/wrapper/gradle-wrapper.properties b/crash/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/crash/gradle/wrapper/gradle-wrapper.properties +++ b/crash/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/database/gradle/wrapper/gradle-wrapper.properties b/database/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/database/gradle/wrapper/gradle-wrapper.properties +++ b/database/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/dataconnect/gradle/wrapper/gradle-wrapper.properties b/dataconnect/gradle/wrapper/gradle-wrapper.properties index 679e3a32f8..57d2b36df0 100644 --- a/dataconnect/gradle/wrapper/gradle-wrapper.properties +++ b/dataconnect/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed May 08 19:29:05 BST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/firebase-ai/app/build.gradle.kts b/firebase-ai/app/build.gradle.kts index e5cfa27106..676d9578a1 100644 --- a/firebase-ai/app/build.gradle.kts +++ b/firebase-ai/app/build.gradle.kts @@ -1,9 +1,19 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.google.services) + alias(libs.plugins.ksp) +} + +configurations.all { + resolutionStrategy { + force("org.jetbrains.kotlin:kotlin-stdlib:2.0.21") + force("org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21") + force("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21") + } } android { @@ -30,8 +40,16 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += listOf( + "-XXLanguage:+ExplicitBackingFields", + "-Xskip-metadata-version-check", + "-Xskip-prerelease-check" + ) } buildFeatures { compose = true @@ -66,6 +84,7 @@ dependencies { implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.extensions) + implementation("com.google.guava:guava:33.3.1-android") // Material for XML-based theme implementation(libs.material) @@ -74,6 +93,10 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.ai) implementation(libs.firebase.ai.ondevice) + implementation(libs.genai.prompt) + implementation(libs.genai.structured.output) + ksp(libs.firebase.ai.ksp.processor) + ksp(libs.genai.structured.compiler) // Image loading implementation(libs.coil.compose) 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 8cbafed1f6..c68eb9bd33 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -1,5 +1,6 @@ package com.google.firebase.quickstart.ai +import kotlin.reflect.KClass import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -26,20 +27,56 @@ import androidx.navigation.NavDestination import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.google.firebase.quickstart.ai.feature.live.BidiViewModel +import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceRoute import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel +import com.google.firebase.quickstart.ai.feature.structured.StructuredOutputRoute +import com.google.firebase.quickstart.ai.feature.structured.StructuredOutputViewModel +import com.google.firebase.quickstart.ai.feature.live.StreamAudioViewModel +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeAudioRoute +import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute +import com.google.firebase.quickstart.ai.feature.live.StreamVideoViewModel +import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationRoute +import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationViewModel +import com.google.firebase.quickstart.ai.feature.text.AudioTranslationRoute +import com.google.firebase.quickstart.ai.feature.text.AudioTranslationViewModel +import com.google.firebase.quickstart.ai.feature.text.AutoFunctionCallRoute +import com.google.firebase.quickstart.ai.feature.text.AutoFunctionCallViewModel import com.google.firebase.quickstart.ai.feature.text.ChatViewModel +import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonRoute +import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonViewModel +import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingRoute +import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingViewModel +import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorRoute +import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorViewModel +import com.google.firebase.quickstart.ai.feature.text.NanoBanana2Route +import com.google.firebase.quickstart.ai.feature.text.NanoBanana2ViewModel +import com.google.firebase.quickstart.ai.feature.text.NanoBananaLiteRoute +import com.google.firebase.quickstart.ai.feature.text.NanoBananaLiteViewModel +import com.google.firebase.quickstart.ai.feature.text.NanoBananaProRoute +import com.google.firebase.quickstart.ai.feature.text.NanoBananaProViewModel +import com.google.firebase.quickstart.ai.feature.text.NanoBananaRoute +import com.google.firebase.quickstart.ai.feature.text.NanoBananaViewModel +import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateRoute import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel +import com.google.firebase.quickstart.ai.feature.text.SvgRoute import com.google.firebase.quickstart.ai.feature.text.SvgViewModel +import com.google.firebase.quickstart.ai.feature.text.TranslationRoute +import com.google.firebase.quickstart.ai.feature.text.TranslationViewModel +import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorRoute +import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorViewModel +import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationRoute +import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationViewModel +import com.google.firebase.quickstart.ai.feature.text.WeatherChatRoute +import com.google.firebase.quickstart.ai.feature.text.WeatherChatViewModel import com.google.firebase.quickstart.ai.ui.ChatScreen +import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen import com.google.firebase.quickstart.ai.ui.ServerPromptScreen import com.google.firebase.quickstart.ai.ui.StreamRealtimeScreen import com.google.firebase.quickstart.ai.ui.StreamRealtimeVideoScreen -import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen +import com.google.firebase.quickstart.ai.ui.StructuredOutputScreen import com.google.firebase.quickstart.ai.ui.SvgScreen import com.google.firebase.quickstart.ai.ui.navigation.FIREBASE_AI_SAMPLES import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen -import com.google.firebase.quickstart.ai.ui.navigation.ScreenType import com.google.firebase.quickstart.ai.ui.theme.FirebaseAILogicTheme class MainActivity : ComponentActivity() { @@ -84,48 +121,32 @@ class MainActivity : ComponentActivity() { } // Add navigation for all of the samples - FIREBASE_AI_SAMPLES.forEach { sample -> - composable( - route = sample.route::class, - typeMap = emptyMap() - ) { - val viewModelClass = sample.viewModelClass?.java - ?: return@composable - val vm = viewModel(modelClass = viewModelClass) - - when (sample.screenType) { - ScreenType.CHAT -> { - (vm as? ChatViewModel)?.let { ChatScreen(it) } - } - - ScreenType.SVG -> { - (vm as? SvgViewModel)?.let { SvgScreen(it) } - } - - ScreenType.SERVER_PROMPT -> { - (vm as? ServerPromptTemplateViewModel)?.let { ServerPromptScreen(it) } - } - - ScreenType.BIDI -> { - (vm as? BidiViewModel)?.let { - @SuppressLint("MissingPermission") - StreamRealtimeScreen(it) - } - } - - ScreenType.BIDI_VIDEO -> { - (vm as? BidiViewModel)?.let { - @SuppressLint("MissingPermission") - StreamRealtimeVideoScreen(it) - } - } - - ScreenType.HYBRID -> { - (vm as? HybridInferenceViewModel)?.let { HybridInferenceScreen(it) } - } - } - } + composable { ChatScreen(viewModel()) } + composable { SvgScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { ChatScreen(viewModel()) } + composable { + @SuppressLint("MissingPermission") + StreamRealtimeScreen(viewModel()) + } + composable { + @SuppressLint("MissingPermission") + StreamRealtimeVideoScreen(viewModel()) } + composable { ServerPromptScreen(viewModel()) } + composable { HybridInferenceScreen(viewModel()) } + composable { StructuredOutputScreen(viewModel()) } } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/MovieReview.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/MovieReview.kt new file mode 100644 index 0000000000..b67977e828 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/MovieReview.kt @@ -0,0 +1,20 @@ +package com.google.firebase.quickstart.ai.feature.structured + +import com.google.firebase.ai.annotations.Generable +import com.google.firebase.ai.annotations.Guide +import kotlinx.serialization.Serializable + +@Generable(description = "A structured review and analysis of a movie or TV show") +@Serializable +data class MovieReview( + @Guide(description = "The official title of the movie or TV show") + val title: String, + @Guide(description = "A concise 1-sentence summary of the critique") + val summary: String, + @Guide(description = "The rating out of 5 stars (1 to 5)") + val rating: Int, + @Guide(description = "Key genre tags or themes") + val tags: List +) { + companion object +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/StructuredOutputViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/StructuredOutputViewModel.kt new file mode 100644 index 0000000000..5c9c607aa1 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/structured/StructuredOutputViewModel.kt @@ -0,0 +1,196 @@ +package com.google.firebase.quickstart.ai.feature.structured + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.Firebase +import com.google.firebase.ai.InferenceMode +import com.google.firebase.ai.OnDeviceConfig +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.JsonSchema +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.generationConfig +import com.google.firebase.quickstart.ai.ui.StructuredOutputUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +object StructuredOutputRoute + +enum class GenerationWorkflow { + GENERATE_OBJECT, // Typed KSP Schema + GENERATE_CONTENT // Manual JsonSchema +} + +@OptIn(PublicPreviewAPI::class) +class StructuredOutputViewModel : ViewModel() { + private val _uiState = MutableStateFlow(StructuredOutputUiState()) + val uiState: StateFlow = _uiState + + private fun getModel(mode: InferenceMode) = + Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = "gemini-3.1-flash-lite", + onDeviceConfig = OnDeviceConfig(mode = mode) + ) + + fun runStructuredInference( + promptText: String, + mode: InferenceMode, + workflow: GenerationWorkflow + ) { + viewModelScope.launch { + _uiState.update { + it.copy( + isLoading = true, + errorMessage = null, + resultObject = null, + rawJson = null, + inferenceSource = null, + logOutput = "Starting inference..." + ) + } + + val modeString = when (mode) { + InferenceMode.PREFER_IN_CLOUD -> "PREFER_IN_CLOUD (Hybrid - Prefer Cloud)" + InferenceMode.PREFER_ON_DEVICE -> "PREFER_ON_DEVICE (Hybrid Fallback)" + InferenceMode.ONLY_ON_DEVICE -> "ONLY_ON_DEVICE (ML Kit)" + InferenceMode.ONLY_IN_CLOUD -> "ONLY_IN_CLOUD (Vertex/Google AI)" + else -> mode.toString() + } + + val schemaDescription = if (workflow == GenerationWorkflow.GENERATE_OBJECT) { + "MovieReview.firebaseAISchema() (KSP Companion Class)" + } else { + "JsonSchema.obj(MovieReview::class) (Manual Schema)" + } + + Log.i(TAG, "==========================================================") + Log.i(TAG, ">>> [MANUAL CHECK - INPUT] >>>") + Log.i(TAG, "Workflow: ${workflow.name}") + Log.i(TAG, "Inference Mode: $modeString") + Log.i(TAG, "Schema Type: $schemaDescription") + Log.i(TAG, "Prompt Text: \"$promptText\"") + Log.i(TAG, "==========================================================") + + try { + val model = getModel(mode) + var parsedObject: MovieReview? = null + var rawTextString: String? = null + var actualSource: String? = null + + when (workflow) { + GenerationWorkflow.GENERATE_OBJECT -> { + Log.d(TAG, "Executing generateObject with KSP schema...") + val schema = MovieReview.firebaseAISchema() + val response = model.generateObject(schema, promptText) + + parsedObject = response.getObject() + rawTextString = response.response.text + actualSource = response.response.inferenceSource?.toString() ?: "UNKNOWN" + } + + GenerationWorkflow.GENERATE_CONTENT -> { + Log.d(TAG, "Executing generateContent with manual schema...") + val manualSchema = JsonSchema.obj( + properties = mapOf( + "title" to JsonSchema.string(), + "summary" to JsonSchema.string(), + "rating" to JsonSchema.integer(), + "tags" to JsonSchema.array(items = JsonSchema.string()) + ), + clazz = MovieReview::class + ) + + val customModel = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = "gemini-3.1-flash-lite", + generationConfig = generationConfig { + responseMimeType = "application/json" + responseJsonSchema = manualSchema + }, + onDeviceConfig = OnDeviceConfig(mode = mode) + ) + + val response = customModel.generateContent(promptText) + rawTextString = response.text + actualSource = response.inferenceSource?.toString() ?: "UNKNOWN" + + if (rawTextString != null) { + val cleanJson = rawTextString + .replace("```json", "") + .replace("```", "") + .trim() + parsedObject = Json.decodeFromString(cleanJson) + } + } + } + + Log.i(TAG, "==========================================================") + Log.i(TAG, "<<< [MANUAL CHECK - OUTPUT (SUCCESS)] <<<") + Log.i(TAG, "Workflow: ${workflow.name}") + Log.i(TAG, "Actual Source: $actualSource") + Log.i(TAG, "Raw Text / JSON: \"${rawTextString ?: "(Empty - ML Kit returned in-memory typed object directly)"}\"") + Log.i(TAG, "Parsed Object: $parsedObject") + Log.i(TAG, "==========================================================") + + val summaryLog = """ + === INPUT === + Schema: $schemaDescription + Prompt: "$promptText" + + === OUTPUT (SUCCESS) === + Source: $actualSource + Parsed Object: + $parsedObject + + Raw Text/JSON: '${rawTextString ?: "(Empty - ML Kit typed object)"}' + """.trimIndent() + + _uiState.update { + it.copy( + isLoading = false, + resultObject = parsedObject, + rawJson = rawTextString, + inferenceSource = actualSource, + logOutput = summaryLog + ) + } + } catch (e: Exception) { + Log.e(TAG, "==========================================================") + Log.e(TAG, "<<< [MANUAL CHECK - OUTPUT (FAILURE)] <<<") + Log.e(TAG, "Workflow: ${workflow.name}") + Log.e(TAG, "Inference Mode: $modeString") + Log.e(TAG, "Exception Type: ${e::class.java.name}") + Log.e(TAG, "Error Message: ${e.localizedMessage ?: e.toString()}") + Log.e(TAG, "==========================================================", e) + + val errorLog = """ + === INPUT === + Schema: $schemaDescription + Prompt: "$promptText" + + === OUTPUT (FAILURE) === + Status: FAILED + Exception: ${e::class.java.simpleName} + Message: ${e.localizedMessage ?: e.toString()} + """.trimIndent() + + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.localizedMessage ?: e.toString(), + logOutput = errorLog + ) + } + } + } + } + + companion object { + private const val TAG = "StructuredOutputDemo" + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputScreen.kt new file mode 100644 index 0000000000..78a56485df --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputScreen.kt @@ -0,0 +1,234 @@ +package com.google.firebase.quickstart.ai.ui + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.firebase.ai.InferenceMode +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.quickstart.ai.feature.structured.GenerationWorkflow +import com.google.firebase.quickstart.ai.feature.structured.StructuredOutputViewModel + +@OptIn(PublicPreviewAPI::class) +@Composable +fun StructuredOutputScreen( + viewModel: StructuredOutputViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + var prompt by remember { mutableStateOf("Write a movie review for Inception (2010)") } + var selectedWorkflow by remember { mutableStateOf(GenerationWorkflow.GENERATE_OBJECT) } + var selectedMode by remember { mutableStateOf(InferenceMode.PREFER_IN_CLOUD) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Structured Output Demo", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + Text( + text = "Test structured output generation across Cloud, ML Kit (On-Device), and Hybrid fallback modes. All inputs and outputs are also logged to Logcat under 'StructuredOutputDemo'.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + OutlinedTextField( + value = prompt, + onValueChange = { prompt = it }, + label = { Text("Prompt") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3 + ) + + Text("1. Select Workflow:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + selected = selectedWorkflow == GenerationWorkflow.GENERATE_OBJECT, + onClick = { selectedWorkflow = GenerationWorkflow.GENERATE_OBJECT }, + label = { Text("generateObject (Typed KSP)") } + ) + FilterChip( + selected = selectedWorkflow == GenerationWorkflow.GENERATE_CONTENT, + onClick = { selectedWorkflow = GenerationWorkflow.GENERATE_CONTENT }, + label = { Text("generateContent (Manual Schema)") } + ) + } + + Text("2. Select Inference Mode:", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + selected = selectedMode == InferenceMode.PREFER_IN_CLOUD, + onClick = { selectedMode = InferenceMode.PREFER_IN_CLOUD }, + label = { Text("PREFER_IN_CLOUD (Hybrid)") } + ) + FilterChip( + selected = selectedMode == InferenceMode.ONLY_ON_DEVICE, + onClick = { selectedMode = InferenceMode.ONLY_ON_DEVICE }, + label = { Text("ONLY_ON_DEVICE (ML Kit)") } + ) + FilterChip( + selected = selectedMode == InferenceMode.ONLY_IN_CLOUD, + onClick = { selectedMode = InferenceMode.ONLY_IN_CLOUD }, + label = { Text("ONLY_IN_CLOUD (Vertex/AI)") } + ) + } + + Button( + onClick = { + viewModel.runStructuredInference(prompt, selectedMode, selectedWorkflow) + }, + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && prompt.isNotBlank() + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.padding(end = 8.dp) + ) + Text("Generating...") + } else { + Text("Run Structured Inference") + } + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + + if (uiState.errorMessage != null) { + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = "Error", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + fontWeight = FontWeight.Bold + ) + Text( + text = uiState.errorMessage ?: "", + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + + val review = uiState.resultObject + if (review != null) { + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = review.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Surface( + color = MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.small + ) { + Text( + text = "${review.rating} ★", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold + ) + } + } + Text( + text = review.summary, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + review.tags.forEach { tag -> + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.extraSmall + ) { + Text( + text = "#$tag", + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + Text( + text = "Inference Source: ${uiState.inferenceSource ?: "UNKNOWN"}", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + } + } + + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = "Diagnostics & Logcat Summary", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Text( + text = uiState.logOutput, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace + ) + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputUiState.kt new file mode 100644 index 0000000000..0b772d9d68 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StructuredOutputUiState.kt @@ -0,0 +1,12 @@ +package com.google.firebase.quickstart.ai.ui + +import com.google.firebase.quickstart.ai.feature.structured.MovieReview + +data class StructuredOutputUiState( + val isLoading: Boolean = false, + val resultObject: MovieReview? = null, + val rawJson: String? = null, + val inferenceSource: String? = null, + val logOutput: String = "Select workflow and inference mode, then tap Generate to test.", + val errorMessage: String? = null +) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt index 069936b0d1..2962d7ebb2 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt @@ -2,6 +2,8 @@ package com.google.firebase.quickstart.ai.ui.navigation import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceRoute import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel +import com.google.firebase.quickstart.ai.feature.structured.StructuredOutputRoute +import com.google.firebase.quickstart.ai.feature.structured.StructuredOutputViewModel import com.google.firebase.quickstart.ai.feature.live.StreamAudioViewModel import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeAudioRoute import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute @@ -197,5 +199,13 @@ val FIREBASE_AI_SAMPLES = listOf( screenType = ScreenType.HYBRID, viewModelClass = HybridInferenceViewModel::class, categories = listOf(Category.HYBRID) + ), + Sample( + title = "Structured & Hybrid Output", + description = "Test generateObject and generateContent across Cloud, On-Device (ML Kit), and Hybrid fallback modes with Logcat logging.", + route = StructuredOutputRoute, + screenType = ScreenType.STRUCTURED, + viewModelClass = StructuredOutputViewModel::class, + categories = listOf(Category.HYBRID, Category.GEMINI3) ) ) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt index d800f01841..714ac7051c 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt @@ -21,7 +21,8 @@ enum class ScreenType { SERVER_PROMPT, BIDI, BIDI_VIDEO, - HYBRID + HYBRID, + STRUCTURED } data class Sample( diff --git a/firebase-ai/build.gradle.kts b/firebase-ai/build.gradle.kts index 11ea2d1c62..b01f63cd5c 100644 --- a/firebase-ai/build.gradle.kts +++ b/firebase-ai/build.gradle.kts @@ -1,9 +1,9 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.google.services) apply false + alias(libs.plugins.ksp) apply false } diff --git a/firebase-ai/gradle/wrapper/gradle-wrapper.properties b/firebase-ai/gradle/wrapper/gradle-wrapper.properties index 5b05b8c3ff..fa9f7bcebf 100644 --- a/firebase-ai/gradle/wrapper/gradle-wrapper.properties +++ b/firebase-ai/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 19 11:04:48 PDT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/firestore/gradle/wrapper/gradle-wrapper.properties b/firestore/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/firestore/gradle/wrapper/gradle-wrapper.properties +++ b/firestore/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/functions/gradle/wrapper/gradle-wrapper.properties b/functions/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/functions/gradle/wrapper/gradle-wrapper.properties +++ b/functions/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bb0ce5c3d..dd9450a705 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,26 @@ [versions] -activityCompose = "1.13.0" -agp = "9.2.1" -camerax = "1.6.1" -coil3Compose = "3.5.0" -composeBom = "2026.06.01" -composeNavigation = "2.9.8" -coreKtx = "1.19.0" -espressoCore = "3.7.0" +activityCompose = "1.9.3" +agp = "8.13.2" +camerax = "1.4.0" +coil3Compose = "3.0.0" +composeBom = "2024.10.01" +composeNavigation = "2.8.3" +coreKtx = "1.13.1" +espressoCore = "3.6.1" firebaseBom = "34.15.0" -googleServices = "4.5.0" +googleServices = "4.4.2" firebaseCrashlytics = "3.0.7" firebasePerf = "2.0.2" +genaiPrompt = "1.0.0-beta3" gradleVersions = "0.54.0" junit = "4.13.2" -junitVersion = "1.3.0" -kotlin = "2.4.0" -kotlinxSerializationCore = "1.11.0" -lifecycle = "2.11.0" -material = "1.14.0" -materialIcons = "1.7.8" -webkit = "1.14.0" +junitVersion = "1.2.1" +kotlin = "2.0.21" +kotlinxSerializationCore = "1.7.3" +lifecycle = "2.8.6" +material = "1.12.0" +materialIcons = "1.7.0" +webkit = "1.12.1" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -32,7 +33,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi 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-compose-android = { module = "androidx.lifecycle:lifecycle-runtime-compose", 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" } @@ -51,8 +52,12 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3C coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3Compose" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3Compose" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"} -firebase-ai = { module = "com.google.firebase:firebase-ai" } -firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta03" } +firebase-ai = { module = "com.google.firebase:firebase-ai", version = "17.14.0" } +firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta04" } +firebase-ai-ksp-processor = { module = "com.google.firebase:firebase-ai-ksp-processor", version = "16.0.2" } +genai-prompt = { module = "com.google.mlkit:genai-prompt", version.ref = "genaiPrompt" } +genai-structured-output = { module = "com.google.mlkit:genai-structured-output", version = "1.0.0-alpha1" } +genai-structured-compiler = { module = "com.google.mlkit:genai-structured-compiler", version = "1.0.0-alpha1" } 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" } @@ -71,3 +76,4 @@ firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = " firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerf" } navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "composeNavigation" } gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" } +ksp = { id = "com.google.devtools.ksp", version = "2.0.21-1.0.25" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a351597e62..4f5eb9dcc0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/inappmessaging/gradle/wrapper/gradle-wrapper.properties b/inappmessaging/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/inappmessaging/gradle/wrapper/gradle-wrapper.properties +++ b/inappmessaging/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/messaging/gradle/wrapper/gradle-wrapper.properties b/messaging/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/messaging/gradle/wrapper/gradle-wrapper.properties +++ b/messaging/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/perf/gradle/wrapper/gradle-wrapper.properties b/perf/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/perf/gradle/wrapper/gradle-wrapper.properties +++ b/perf/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/storage/gradle/wrapper/gradle-wrapper.properties b/storage/gradle/wrapper/gradle-wrapper.properties index 52ad5e7158..b41387345b 100644 --- a/storage/gradle/wrapper/gradle-wrapper.properties +++ b/storage/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists