From e37be5ade016a98189b1d05b8e970c949b1ff1a5 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Fri, 21 Feb 2025 19:32:46 +0000 Subject: [PATCH 001/162] add new recipe entries to BoM generator (#6702) Co-authored-by: David Motsonashvili --- .../gradle/bomgenerator/GenerateTutorialBundleTask.kt | 6 ++++++ .../google/firebase/gradle/plugins/PublishingPlugin.kt | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt index b315e7188aa..e99565d47f7 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt @@ -257,6 +257,12 @@ abstract class GenerateTutorialBundleTask : DefaultTask() { ArtifactTutorialMapping("FIAM Display", "fiamd-dependency"), "com.google.firebase:firebase-ml-vision" to ArtifactTutorialMapping("Firebase MLKit Vision", "ml-vision-dependency"), + "androidx.credentials:credentials" to + ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-first-dependency"), + "androidx.credentials:credentials-play-services-auth" to + ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-second-dependency"), + "com.google.android.libraries.identity.googleid" to + ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-third-dependency"), "com.google.firebase:firebase-appdistribution-gradle" to ArtifactTutorialMapping( "App Distribution", diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt index 82a2aaae858..e8384b4579f 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt @@ -820,7 +820,13 @@ abstract class PublishingPlugin : Plugin { /** Artifacts that we use in the tutorial bundle, but _not_ in the bom. */ val EXTRA_TUTORIAL_ARTIFACTS = - listOf("com.google.android.gms:play-services-ads", "com.google.firebase:firebase-ml-vision") + listOf( + "com.google.android.gms:play-services-ads", + "com.google.firebase:firebase-ml-vision", + "androidx.credentials:credentials", + "androidx.credentials:credentials-play-services-auth", + "com.google.android.libraries.identity.googleid:googleid", + ) } } From aa97aa3aab2b2460678f82091747e3cde4d2500e Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Feb 2025 10:51:39 -0500 Subject: [PATCH 002/162] Improve imagen Java API (#6712) - Make the `ImagenGenerationConfig.Builder` follow the builder pattern - Mark companion object `ImagenImageFormat` methods as @JvmStatic for easier access --- firebase-vertexai/api.txt | 8 ++++ .../vertexai/type/ImagenGenerationConfig.kt | 45 ++++++++++++++++--- .../vertexai/type/ImagenImageFormat.kt | 2 + 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt index abfbf6572d4..01a7204ac73 100644 --- a/firebase-vertexai/api.txt +++ b/firebase-vertexai/api.txt @@ -428,6 +428,11 @@ package com.google.firebase.vertexai.type { public static final class ImagenGenerationConfig.Builder { ctor public ImagenGenerationConfig.Builder(); method public com.google.firebase.vertexai.type.ImagenGenerationConfig build(); + method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setAddWatermark(boolean addWatermark); + method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setAspectRatio(com.google.firebase.vertexai.type.ImagenAspectRatio aspectRatio); + method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setImageFormat(com.google.firebase.vertexai.type.ImagenImageFormat imageFormat); + method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setNegativePrompt(String negativePrompt); + method public com.google.firebase.vertexai.type.ImagenGenerationConfig.Builder setNumberOfImages(int numberOfImages); field public Boolean? addWatermark; field public com.google.firebase.vertexai.type.ImagenAspectRatio? aspectRatio; field public com.google.firebase.vertexai.type.ImagenImageFormat? imageFormat; @@ -441,6 +446,7 @@ package com.google.firebase.vertexai.type { public final class ImagenGenerationConfigKt { method @com.google.firebase.vertexai.type.PublicPreviewAPI public static com.google.firebase.vertexai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 init); + method public static void xx(); } @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationResponse { @@ -453,6 +459,8 @@ package com.google.firebase.vertexai.type { @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenImageFormat { method public Integer? getCompressionQuality(); method public String getMimeType(); + method public static com.google.firebase.vertexai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null); + method public static com.google.firebase.vertexai.type.ImagenImageFormat png(); property public final Integer? compressionQuality; property public final String mimeType; field public static final com.google.firebase.vertexai.type.ImagenImageFormat.Companion Companion; diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt index bbf795f4d40..7f5b935bd1f 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt @@ -26,6 +26,8 @@ package com.google.firebase.vertexai.type * @param imageFormat The file format/compression of the generated images. * @param addWatermark Adds an invisible watermark to mark the image as AI generated. */ +import kotlin.jvm.JvmField + @PublicPreviewAPI public class ImagenGenerationConfig( public val negativePrompt: String? = null, @@ -39,13 +41,6 @@ public class ImagenGenerationConfig( * * This is mainly intended for Java interop. For Kotlin, use [imagenGenerationConfig] for a more * idiomatic experience. - * - * @property negativePrompt See [ImagenGenerationConfig.negativePrompt]. - * @property numberOfImages See [ImagenGenerationConfig.numberOfImages]. - * @property aspectRatio See [ImagenGenerationConfig.aspectRatio]. - * @property imageFormat See [ImagenGenerationConfig.imageFormat] - * @property addWatermark See [ImagenGenerationConfig.addWatermark] - * @see [imagenGenerationConfig] */ public class Builder { @JvmField public var negativePrompt: String? = null @@ -54,6 +49,31 @@ public class ImagenGenerationConfig( @JvmField public var imageFormat: ImagenImageFormat? = null @JvmField public var addWatermark: Boolean? = null + /** See [ImagenGenerationConfig.negativePrompt]. */ + public fun setNegativePrompt(negativePrompt: String): Builder = apply { + this.negativePrompt = negativePrompt + } + + /** See [ImagenGenerationConfig.numberOfImages]. */ + public fun setNumberOfImages(numberOfImages: Int): Builder = apply { + this.numberOfImages = numberOfImages + } + + /** See [ImagenGenerationConfig.aspectRatio]. */ + public fun setAspectRatio(aspectRatio: ImagenAspectRatio): Builder = apply { + this.aspectRatio = aspectRatio + } + + /** See [ImagenGenerationConfig.imageFormat]. */ + public fun setImageFormat(imageFormat: ImagenImageFormat): Builder = apply { + this.imageFormat = imageFormat + } + + /** See [ImagenGenerationConfig.addWatermark]. */ + public fun setAddWatermark(addWatermark: Boolean): Builder = apply { + this.addWatermark = addWatermark + } + /** * Alternative casing for [ImagenGenerationConfig.Builder]: * ``` @@ -97,3 +117,14 @@ public fun imagenGenerationConfig( builder.init() return builder.build() } + +@OptIn(PublicPreviewAPI::class) +public fun xx() { + imagenGenerationConfig { + negativePrompt = "People, black and white, painting" + numberOfImages = 1 + aspectRatio = ImagenAspectRatio.SQUARE_1x1 + imageFormat = ImagenImageFormat.png() + addWatermark = false + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt index 41c85e98a7a..5a44ddc3964 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenImageFormat.kt @@ -41,11 +41,13 @@ private constructor(public val mimeType: String, public val compressionQuality: * @param compressionQuality an int (1-100) representing the quality of the image; a lower * number means the image is permitted to be lower quality to reduce size. */ + @JvmStatic public fun jpeg(compressionQuality: Int? = null): ImagenImageFormat { return ImagenImageFormat("image/jpeg", compressionQuality) } /** An [ImagenImageFormat] representing a PNG image */ + @JvmStatic public fun png(): ImagenImageFormat { return ImagenImageFormat("image/png", null) } From 680ce8cb7a77337ff6d8c788b2025701a7f6c047 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Feb 2025 12:46:02 -0500 Subject: [PATCH 003/162] Remove test code left by mistake (#6717) Should be more careful with those changes... --- firebase-vertexai/api.txt | 1 - .../firebase/vertexai/type/ImagenGenerationConfig.kt | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt index 01a7204ac73..ecf5ab8eefc 100644 --- a/firebase-vertexai/api.txt +++ b/firebase-vertexai/api.txt @@ -446,7 +446,6 @@ package com.google.firebase.vertexai.type { public final class ImagenGenerationConfigKt { method @com.google.firebase.vertexai.type.PublicPreviewAPI public static com.google.firebase.vertexai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 init); - method public static void xx(); } @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ImagenGenerationResponse { diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt index 7f5b935bd1f..d05840d9cfc 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationConfig.kt @@ -117,14 +117,3 @@ public fun imagenGenerationConfig( builder.init() return builder.build() } - -@OptIn(PublicPreviewAPI::class) -public fun xx() { - imagenGenerationConfig { - negativePrompt = "People, black and white, painting" - numberOfImages = 1 - aspectRatio = ImagenAspectRatio.SQUARE_1x1 - imageFormat = ImagenImageFormat.png() - addWatermark = false - } -} From 22be120f823c155ed6c49b814f85c760289ef1f6 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Feb 2025 13:30:27 -0500 Subject: [PATCH 004/162] Add missing optIn declarations to reduce compilation noise (#6713) Part of the serialization API we use requires optIn, and without the correct declarations we get warnings printed when compiling the code. --- .../com/google/firebase/vertexai/GenerativeModel.kt | 2 ++ .../com/google/firebase/vertexai/common/Request.kt | 10 ++-------- .../com/google/firebase/vertexai/type/Candidate.kt | 2 ++ .../com/google/firebase/vertexai/type/Content.kt | 1 + .../google/firebase/vertexai/GenerativeModelTesting.kt | 2 ++ .../firebase/vertexai/common/APIControllerTests.kt | 2 ++ .../firebase/vertexai/common/StreamingSnapshotTests.kt | 2 ++ 7 files changed, 13 insertions(+), 8 deletions(-) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt index a49d4c279a8..12d89ab5b59 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt @@ -40,6 +40,7 @@ import com.google.firebase.vertexai.type.content import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map +import kotlinx.serialization.ExperimentalSerializationApi /** * Represents a multimodal model (like Gemini), capable of generating content based on various input @@ -199,6 +200,7 @@ internal constructor( return countTokens(content { image(prompt) }) } + @OptIn(ExperimentalSerializationApi::class) private fun constructRequest(vararg prompt: Content) = GenerateContentRequest( modelName, diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt index 8696a090fc2..7f84e053147 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalSerializationApi::class) package com.google.firebase.vertexai.common @@ -24,6 +25,7 @@ import com.google.firebase.vertexai.type.PublicPreviewAPI import com.google.firebase.vertexai.type.SafetySetting import com.google.firebase.vertexai.type.Tool import com.google.firebase.vertexai.type.ToolConfig +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -49,14 +51,6 @@ internal data class CountTokensRequest( @SerialName("system_instruction") val systemInstruction: Content.Internal? = null, ) : Request { companion object { - fun forGenAI(generateContentRequest: GenerateContentRequest) = - CountTokensRequest( - generateContentRequest = - generateContentRequest.model?.let { - generateContentRequest.copy(model = fullModelName(it)) - } - ?: generateContentRequest - ) fun forVertexAI(generateContentRequest: GenerateContentRequest) = CountTokensRequest( diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt index 5d236c8ecc9..b84bd6929f4 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Candidate.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalSerializationApi::class) + package com.google.firebase.vertexai.type import com.google.firebase.vertexai.common.util.FirstOrdinalSerializer diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt index 241d0becfe6..9364f9cad3c 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Content.kt @@ -80,6 +80,7 @@ constructor(public val role: String? = "user", public val parts: List) { public fun build(): Content = Content(role, parts) } + @OptIn(ExperimentalSerializationApi::class) internal fun toInternal() = Internal(this.role ?: "user", this.parts.map { it.toInternal() }) @ExperimentalSerializationApi diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt index 67d41c9b5d6..d4c2ad37926 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt @@ -40,6 +40,7 @@ import io.ktor.http.content.TextContent import io.ktor.http.headersOf import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import org.junit.Test @@ -127,6 +128,7 @@ internal class GenerativeModelTesting { exception.message shouldContain "location" } + @OptIn(ExperimentalSerializationApi::class) private fun generateContentResponseAsJsonString(text: String): String { return JSON.encodeToString( GenerateContentResponse.Internal( diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt index 463dbe773f7..29b52b81d1b 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt @@ -46,6 +46,7 @@ import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonObject import org.junit.Test @@ -84,6 +85,7 @@ internal class APIControllerTests { } } +@OptIn(ExperimentalSerializationApi::class) internal class RequestFormatTests { @Test fun `using default endpoint`() = doBlocking { diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt index 6d14025b28c..4abf386765a 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt @@ -30,8 +30,10 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withTimeout +import kotlinx.serialization.ExperimentalSerializationApi import org.junit.Test +@OptIn(ExperimentalSerializationApi::class) internal class StreamingSnapshotTests { private val testTimeout = 5.seconds From a2244f976ed16e96defb09d8fda253963864c747 Mon Sep 17 00:00:00 2001 From: Konstantin Svist Date: Mon, 24 Feb 2025 12:00:27 -0800 Subject: [PATCH 005/162] Support custom tabs in more browsers (#6705) fixes #6692 --- firebase-appdistribution/src/main/AndroidManifest.xml | 6 ++++++ .../appdistribution/impl/TesterSignInManager.java | 11 ++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/firebase-appdistribution/src/main/AndroidManifest.xml b/firebase-appdistribution/src/main/AndroidManifest.xml index ef91581edc3..452fe856e6c 100644 --- a/firebase-appdistribution/src/main/AndroidManifest.xml +++ b/firebase-appdistribution/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + + + + + resolveInfos = - context.getPackageManager().queryIntentServices(customTabIntent, 0); - return resolveInfos != null && !resolveInfos.isEmpty(); + String packageName = CustomTabsClient.getPackageName(context, Collections.emptyList()); + return packageName != null; } } From 5712a26d16fc8b68177fe81d40580e5bbc756c4a Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 24 Feb 2025 15:32:20 -0500 Subject: [PATCH 006/162] Update CHANGELOG for Crashlytics and NDK (#6719) --- firebase-crashlytics-ndk/CHANGELOG.md | 2 +- firebase-crashlytics/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index b5b8f7868d4..ab8dd8dfdf3 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [changed] Updated `firebase-crashlytics` dependency to v19.4.1 # 19.3.0 * [changed] Updated `firebase-crashlytics` dependency to v19.3.0 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 7086b0b0c9d..f0bbb91128b 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [changed] Updated `firebase-sessions` dependency to v2.0.9 # 19.4.0 * [feature] Added an overload for `recordException` that allows logging additional custom From 4cf282576e2fec70d8088fab024130b5ec5dda4c Mon Sep 17 00:00:00 2001 From: Daymon <17409137+daymxn@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:37:26 -0600 Subject: [PATCH 007/162] Bump well known types (#6716) Per [b/398840288](https://b.corp.google.com/issues/398840288), This bumps `protolite-well-known-types` to properly utilize `3.25.5`. It seems as though this was an oversight in #6343, but since gradle uses the highest version when resolving dependency conflicts (and all the existing libraries already use `3.25.5`), this isn't a major issue. This is only really an issue if someone is using `protolite-well-known-types` in isolation (which isn't really a use-case we're shipping for). But the main reason for fixing this is that it causes a bit of confusion when trying to track dependency issues (see issue #6674 for an example of this). Fixes #6674 --- firebase-firestore/CHANGELOG.md | 1 + firebase-inappmessaging-display/CHANGELOG.md | 1 + firebase-inappmessaging/CHANGELOG.md | 1 + firebase-perf/CHANGELOG.md | 1 + firebase-perf/firebase-perf.gradle | 2 +- protolite-well-known-types/CHANGELOG.md | 2 +- protolite-well-known-types/README.md | 2 +- protolite-well-known-types/gradle.properties | 2 +- .../protolite-well-known-types.gradle | 7 ++++--- 9 files changed, 12 insertions(+), 7 deletions(-) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 66fce5b35ce..f14e653e79f 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] # 25.1.2 diff --git a/firebase-inappmessaging-display/CHANGELOG.md b/firebase-inappmessaging-display/CHANGELOG.md index 15bd2abe75a..a9b37cf7f10 100644 --- a/firebase-inappmessaging-display/CHANGELOG.md +++ b/firebase-inappmessaging-display/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] # 21.0.1 diff --git a/firebase-inappmessaging/CHANGELOG.md b/firebase-inappmessaging/CHANGELOG.md index 90b3e93ccae..1252d73f787 100644 --- a/firebase-inappmessaging/CHANGELOG.md +++ b/firebase-inappmessaging/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] # 21.0.1 diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 2112244a524..58bdd0f1d29 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] # 21.0.4 diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index b6028e75b61..c0fd6df6056 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -111,7 +111,7 @@ dependencies { implementation libs.dagger.dagger api 'com.google.firebase:firebase-annotations:16.2.0' api 'com.google.firebase:firebase-installations-interop:17.1.0' - api 'com.google.firebase:protolite-well-known-types:18.0.0' + api project(":protolite-well-known-types") implementation libs.okhttp api("com.google.firebase:firebase-common:21.0.0") api("com.google.firebase:firebase-common-ktx:21.0.0") diff --git a/protolite-well-known-types/CHANGELOG.md b/protolite-well-known-types/CHANGELOG.md index f514bbb890e..9947693ea08 100644 --- a/protolite-well-known-types/CHANGELOG.md +++ b/protolite-well-known-types/CHANGELOG.md @@ -1,3 +1,3 @@ # Unreleased - +* [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). diff --git a/protolite-well-known-types/README.md b/protolite-well-known-types/README.md index 30926d080d8..c2691086078 100644 --- a/protolite-well-known-types/README.md +++ b/protolite-well-known-types/README.md @@ -73,7 +73,7 @@ android { } dependencies { - implementation 'com.google.firebase:protolite-well-known-types:18.0.0' + implementation 'com.google.firebase:protolite-well-known-types:18.0.1' implementation "io.grpc:grpc-stub:$grpcVersion" // optionally override grpc's protobuf-lite runtime diff --git a/protolite-well-known-types/gradle.properties b/protolite-well-known-types/gradle.properties index a60ca35eca9..d7239d0c4fe 100644 --- a/protolite-well-known-types/gradle.properties +++ b/protolite-well-known-types/gradle.properties @@ -1,5 +1,5 @@ # IMPORTANT (b/285892320) Keep version and latestReleasedVersion in sync # unless you are releasing a new version of the library to prevent issues # with transitive dependencies. -version=18.0.0 +version=18.0.1 latestReleasedVersion=18.0.0 diff --git a/protolite-well-known-types/protolite-well-known-types.gradle b/protolite-well-known-types/protolite-well-known-types.gradle index f5e5bdd8ff2..fe73979660b 100644 --- a/protolite-well-known-types/protolite-well-known-types.gradle +++ b/protolite-well-known-types/protolite-well-known-types.gradle @@ -26,7 +26,7 @@ firebaseLibrary { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.21.11" + artifact = libs.protoc.get().toString() } generateProtoTasks { all().each { task -> @@ -41,6 +41,7 @@ protobuf { } } } + android { namespace "firebase.com.protolitewrapper" compileSdkVersion project.compileSdkVersion @@ -64,9 +65,9 @@ android { dependencies { - protobuf("com.google.api.grpc:proto-google-common-protos:1.18.0"){ + protobuf(libs.proto.google.common.protos){ exclude group: "com.google.protobuf", module: "protobuf-java" } - implementation "com.google.protobuf:protobuf-javalite:3.21.11" + implementation libs.protobuf.java.lite } From 1e8c2185411d6b62e8a6a74de91d4dccf40838c7 Mon Sep 17 00:00:00 2001 From: Lee Kellogg Date: Mon, 24 Feb 2025 15:39:07 -0500 Subject: [PATCH 008/162] Update CHANGELOG.md (#6718) For this fix: https://github.com/firebase/firebase-android-sdk/pull/6705 --- firebase-appdistribution/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-appdistribution/CHANGELOG.md b/firebase-appdistribution/CHANGELOG.md index 6aa5cbfad70..3bfc9628fd5 100644 --- a/firebase-appdistribution/CHANGELOG.md +++ b/firebase-appdistribution/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [fixed] Added custom tab support for more browsers [#6692] # 16.0.0-beta14 * [changed] Internal improvements to testing on Android 14 From c3308a3adde779ba557963ab2f3381fa297d375f Mon Sep 17 00:00:00 2001 From: Tushar Khandelwal <64364243+tusharkhandelwal8@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:21:19 +0530 Subject: [PATCH 009/162] Add custom signal limits link and fix Javadoc List Formatting (#6722) Add link to documentation about custom signal limits ([b/385028620](https://buganizer.corp.google.com/issues/385028620)) and Update setCustomSignals Javadoc List Formatting ([b/390054823](https://buganizer.corp.google.com/issues/390054823)) --- .../firebase/remoteconfig/FirebaseRemoteConfig.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 808892e7521..abd09dd0330 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -656,16 +656,17 @@ private Task setDefaultsWithStringsMapAsync(Map defaultsSt * Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance. * *

Custom signals are subject to limits on the size of key/value pairs and the total - * number of signals. Any calls that exceed these limits will be discarded. + * number of signals. Any calls that exceed these limits will be discarded. See Custom + * Signal Limits. * * @param customSignals The custom signals to set for this instance. - *

    + *
      *
    • New keys will add new key-value pairs in the custom signals. *
    • Existing keys with new values will update the corresponding signals. *
    • Setting a key's value to {@code null} will remove the associated signal. - *
+ * */ - // TODO(b/385028620): Add link to documentation about custom signal limits. @NonNull public Task setCustomSignals(@NonNull CustomSignals customSignals) { return Tasks.call( From 91ea30ee9ae4fce98c876f4467e43c323410b4e6 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Wed, 26 Feb 2025 19:20:10 +0000 Subject: [PATCH 010/162] Fix documentation for ImagenGenerationResponse (#6728) Co-authored-by: David Motsonashvili --- .../google/firebase/vertexai/type/ImagenGenerationResponse.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt index a1a80360848..dfc011b58f9 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt @@ -16,10 +16,11 @@ package com.google.firebase.vertexai.type +import com.google.firebase.vertexai.ImagenModel import kotlinx.serialization.Serializable /** - * Represents a response from a call to [ImagenModel#generateImages] + * Represents a response from a call to [ImagenModel.generateImages] * * @param images contains the generated images * @param filteredReason if fewer images were generated than were requested, this field will contain From ba0941bd9a49402f253eb2ae2df0894d64442dd8 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:42:31 -0600 Subject: [PATCH 011/162] Add copyApiTxtFile task (#6724) Prerequisite of Metalava based SemVer --- .../firebase/gradle/plugins/CopyApiTask.kt | 33 +++++++++++++++++++ .../plugins/FirebaseAndroidLibraryPlugin.kt | 5 +++ .../plugins/FirebaseJavaLibraryPlugin.kt | 5 +++ .../plugins/FirebaseLibraryExtension.kt | 3 ++ 4 files changed, 46 insertions(+) create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt new file mode 100644 index 00000000000..2de8c01cd22 --- /dev/null +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/CopyApiTask.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.gradle.plugins + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class CopyApiTask : DefaultTask() { + @get:InputFile abstract val apiTxtFile: RegularFileProperty + @get:OutputFile abstract val output: RegularFileProperty + + @TaskAction + fun run() { + output.get().asFile.writeText(apiTxtFile.get().asFile.readText()) + } +} diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt index e0732174ae4..5ec8dcb22cf 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt @@ -160,6 +160,11 @@ class FirebaseAndroidLibraryPlugin : BaseFirebaseLibraryPlugin() { .getLatestReleasedVersion() ) } + + project.tasks.register("copyApiTxtFile") { + apiTxtFile.set(project.file("api.txt")) + output.set(project.file("previous_api.txt")) + } } private fun setupApiInformationAnalysis(project: Project, android: LibraryExtension) { diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt index 0c7b2028f1c..0068f76e677 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt @@ -103,6 +103,11 @@ class FirebaseJavaLibraryPlugin : BaseFirebaseLibraryPlugin() { dependsOn("copyPreviousArtifacts") } + + project.tasks.register("copyApiTxtFile") { + apiTxtFile.set(project.file("api.txt")) + output.set(project.file("previous_api.txt")) + } } private fun setupApiInformationAnalysis(project: Project) { diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt index 8da2d33fb5c..ecc69e0e5a4 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.kt @@ -241,6 +241,9 @@ constructor(val project: Project, val type: LibraryType) { val version: String get() = project.version.toString() + val previousVersion: String + get() = project.properties["latestReleasedVersion"].toString() + val path: String = project.path val runtimeClasspath: String = From 043bdc01da264d62bc596ff223d37b65dd2309de Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 26 Feb 2025 20:26:15 +0000 Subject: [PATCH 012/162] dataconnect: minor cosmetic changes to the github actions workflow (#6727) --- .github/workflows/dataconnect.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml index 3a0b9aa4b93..797f112fd61 100644 --- a/.github/workflows/dataconnect.yml +++ b/.github/workflows/dataconnect.yml @@ -217,10 +217,10 @@ jobs: if-no-files-found: warn compression-level: 9 - - name: Check test result + - name: Verify "Gradle connectedCheck" step was successful if: steps.connectedCheck.outcome != 'success' run: | - echo "Failing the job since the connectedCheck step failed" + echo 'Failing because the outcome of the "Gradle connectedCheck" step ("${{ steps.connectedCheck.outcome }}") was not successful' exit 1 # Check this yml file with "actionlint": https://github.com/rhysd/actionlint From 72c0dcb9173d3bb3c2ece250ef5388aa2e064f55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:03:29 -0500 Subject: [PATCH 013/162] Bump truth from 1.4.2 to 1.4.4 (#6723) Bumps `truth` from 1.4.2 to 1.4.4. Updates `com.google.truth:truth` from 1.4.2 to 1.4.4
Release notes

Sourced from com.google.truth:truth's releases.

v1.4.4

  • Annotated the rest of the main package for nullness, and moved the @NullMarked annotation from individual classes up to the package to avoid a warning under --release 8. (e107aeadc)
  • Improved the failure message for matches to conditionally suggest using containsMatch. (7e9fc7aec)

1.4.3

Known Issue for at least some builds targeting Java 8, fixed in 1.4.4: "unknown enum constant ElementType.MODULE": google/truth#1320. As far as we know, this is only a warning, so it should cause practical problems only if you use -Werror or you perform reflection on @NullMarked under a Java 8 runtime.

  • Added more nullness information to our APIs (in the form of JSpecify annotations). This could lead to additional warnings (or even errors) for users of Kotlin and other nullness checkers. Please report any problems. (ee680cbaf)
  • Deprecated Subject.Factory methods for Java 8 types. We won't remove them, but you can simplify your code by migrating off them: Just replace assertAbout(foos()).that(foo) with assertThat(foo) (or about(foos()).that(foo) with that(foo)). (59e7a5065)
Commits
  • ddeaa0c Set version number for truth-parent to 1.4.4.
  • e107aea Annotate the rest of the main package (basically just the Java 8 subjects) fo...
  • 8ac91a6 Document that truth-java8-extension is obsolete.
  • 99af8be Bump org.codehaus.mojo:animal-sniffer-maven-plugin from 1.23 to 1.24 in the d...
  • 54e548c Bump the dependencies group with 2 updates
  • 2183a14 Migrate from legacy com.google.gwt to org.gwtproject.
  • 7e9fc7a Make StringSubject.matches suggest using containsMatch if matches(x) fails bu...
  • af140d6 Fix grammar in Javadoc comments.
  • afda443 Annotate formattingDiffsUsing methods as supporting nullable element/value ...
  • ee680cb Use JSpecify annotations in the public release.
  • Additional commits viewable in compare view

Updates `com.google.truth.extensions:truth-liteproto-extension` from 1.4.2 to 1.4.4 Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21442a483d3..46d4995cbd8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,7 +71,7 @@ serialization-plugin = "1.8.22" slf4jNop = "2.0.9" spotless = "7.0.0.BETA3" testServices = "1.2.0" -truth = "1.4.2" +truth = "1.4.4" truthProtoExtension = "1.0" wiremockStandalone = "2.26.3" From 9f96db82c306c9699e1f486bd8621d76ec6e3132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:46:16 -0500 Subject: [PATCH 014/162] Bump semver from 7.5.0 to 7.5.4 in /smoke-tests/src/androidTest/backend/functions/functions (#5164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [semver](https://github.com/npm/node-semver) from 7.5.0 to 7.5.4.
Release notes

Sourced from semver's releases.

v7.5.4

7.5.4 (2023-07-07)

Bug Fixes

v7.5.3

7.5.3 (2023-06-22)

Bug Fixes

Documentation

v7.5.2

7.5.2 (2023-06-15)

Bug Fixes

v7.5.1

7.5.1 (2023-05-12)

Bug Fixes

Changelog

Sourced from semver's changelog.

7.5.4 (2023-07-07)

Bug Fixes

7.5.3 (2023-06-22)

Bug Fixes

Documentation

7.5.2 (2023-06-15)

Bug Fixes

7.5.1 (2023-05-12)

Bug Fixes

Commits
  • 36cd334 chore: release 7.5.4
  • 8456d87 chore: postinstall for dependabot template-oss PR
  • dde1f00 chore: postinstall for dependabot template-oss PR
  • dffcd1b chore: bump @​npmcli/template-oss from 4.16.0 to 4.17.0
  • d619f66 chore: postinstall for dependabot template-oss PR
  • 3bc4247 chore: bump @​npmcli/template-oss from 4.15.1 to 4.16.0
  • cc6fde2 fix: trim each range set before parsing
  • 99d8287 fix: correctly parse long build ids as valid (#583)
  • 4f0f6b1 chore: fix arguments in whitespace test (#574)
  • 6bd1a37 chore: remove duplicate test in semver class (#575)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=npm_and_yarn&previous-version=7.5.0&new-version=7.5.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/firebase/firebase-android-sdk/network/alerts).
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo --- .../backend/functions/functions/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json b/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json index ca5804fab55..9c9d1d98854 100644 --- a/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json +++ b/smoke-tests/src/androidTest/backend/functions/functions/package-lock.json @@ -2711,9 +2711,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5415,9 +5415,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } From 3e541075daa5f4399a5871466aa9217497ae5e65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:32:38 +0000 Subject: [PATCH 015/162] build(deps): bump com.fasterxml.jackson.core:jackson-databind from 2.13.1 to 2.18.2 (#6591) Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.13.1 to 2.18.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.fasterxml.jackson.core:jackson-databind&package-manager=gradle&previous-version=2.13.1&new-version=2.18.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46d4995cbd8..b234d7bbd0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ hamcrestLibrary = "2.2" httpclientAndroid = "4.3.5.1" integrity = "1.2.0" jacksonCore = "2.13.1" -jacksonDatabind = "2.13.1" +jacksonDatabind = "2.18.2" javalite = "3.25.5" jsonassert = "1.5.0" kotest = "5.9.0" # Do not use 5.9.1 because it reverts the fix for https://github.com/kotest/kotest/issues/3981 From 00c2919000386b0f9f292e1d98349b14e46c5f44 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 27 Feb 2025 20:09:03 +0000 Subject: [PATCH 016/162] dataconnect: DataConnectExecutableVersions.json updated with versions 1.8.0, 1.8.1, 1.8.2, and 1.8.3 (#6732) --- .../plugin/DataConnectExecutableVersions.json | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json index 1854796df5e..bc2038801fe 100644 --- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json @@ -1,5 +1,5 @@ { - "defaultVersion": "1.7.7", + "defaultVersion": "1.8.3", "versions": [ { "version": "1.3.4", @@ -414,6 +414,78 @@ "os": "linux", "size": 25268376, "sha512DigestHex": "f55feb1ce670b4728bb30be138ab427545f77f63f9e11ee458096091c075699c647d5b768c642a1ef6b3569a2db87dbbed6f2fdaf64febd1154d1a730fda4a9c" + }, + { + "version": "1.8.0", + "os": "windows", + "size": 25903616, + "sha512DigestHex": "753a5e4be35c544317bcdbaaa860f079a9c9d8a24ca3db17fed601d30b64f083a9203fbb76718d23f3ad77f1556adfb5a4226751ec48c202bd227479c57d1ae9" + }, + { + "version": "1.8.0", + "os": "macos", + "size": 25469696, + "sha512DigestHex": "23c1e405b196799a7c84b9783ca110459bba3aa86405d2fc03d83f90530642d590b02cd06588a8428e0e7bb7d1c59e6d03113bbc5c41e12cff7a7c46674fc430" + }, + { + "version": "1.8.0", + "os": "linux", + "size": 25383064, + "sha512DigestHex": "9546bb62d54b67086847d3e129397f4cfceb5b715d64f0a1cc0a053b5dfe918e8372142b7e9bacd11dede77ddd17840058efb8ed6a7073e99fd5a684fdc57bea" + }, + { + "version": "1.8.1", + "os": "windows", + "size": 25904128, + "sha512DigestHex": "26dc987e38d5d07a910da647920cc2fe990f1da0db56206def71a9833f8eeb66272d8f32ba091b0d4d6e065a3d5cd950cd835a891895c6a55d735a6f240bf4b7" + }, + { + "version": "1.8.1", + "os": "macos", + "size": 25469696, + "sha512DigestHex": "d7bcb01912b1949a003fd0a7ebbc1bb42e79e97b7fd880ba9164b62e05d1ffb634662d97fd4664343e28780e69953aadecd5fb799a8f51229a4c0fbf552936ac" + }, + { + "version": "1.8.1", + "os": "linux", + "size": 25383064, + "sha512DigestHex": "2a28ba7947f84ede9062b5f5efa145b29862be0a8724ac6b6a4210f6823024d33363bd3379a6474965fbd60376baae9103ce7e4509db9d52c2b13886bca5df92" + }, + { + "version": "1.8.2", + "os": "windows", + "size": 25936384, + "sha512DigestHex": "f2aed75baaeed388d8fcd8a3d18e629f9ed012f60de0401bc365227094688f130ce7aa02db565002fe7b06a339b1cb133a7c87da365d480fb10cdb47d55c7dfa" + }, + { + "version": "1.8.2", + "os": "macos", + "size": 25506560, + "sha512DigestHex": "d4ac9e15f5a42fed28fd2f3cb2c80bc3f4def60f76517661323c502fa7a4b085bda3d26eb62cdcb630a13999e2fb0428ee45d335e20641229a9439cc60a9e798" + }, + { + "version": "1.8.2", + "os": "linux", + "size": 25415832, + "sha512DigestHex": "fec0fb97fb3ad30bdd9d0e3b65095e2dfdcfccd15e7c6ae9fe827ec1c3b5b9b592c80c59cadb3540e387d4adcf3560922094399c5ca3d162288a33403308104d" + }, + { + "version": "1.8.3", + "os": "windows", + "size": 25965568, + "sha512DigestHex": "9b6ded9ddac61d5f137ac65944409003906d621bb3a03ba6bf037b1aeddabf23f9410de6fbc05b8ea0c9afa2a8328bb02a57ed225f8ebaa3c8d6921755ad715c" + }, + { + "version": "1.8.3", + "os": "macos", + "size": 25535232, + "sha512DigestHex": "0c88a14ae64308e68957f5e79f9e20b4b946977187132dcc24193370c81b9117487fb0ee1c5be4e8f2368945add7ed37d6d97b015c3ea8232e09664458c8e802" + }, + { + "version": "1.8.3", + "os": "linux", + "size": 25448600, + "sha512DigestHex": "6734188ed2dc41fdf9922e152848d46a4bd6a30083c918ac0de5197e1f998f8dc2b4e190c47b02c176f68b93591132c29be142b4b61a36c81aec2358a81864c6" } ] } \ No newline at end of file From 16d2ba862415b232a75b7135b893d11b0f382038 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 27 Feb 2025 21:42:33 +0000 Subject: [PATCH 017/162] dataconnect: change grpc api version from "v1beta" to "v1" (#6729) --- firebase-dataconnect/CHANGELOG.md | 3 ++- .../demo/firebase/dataconnect/dataconnect.yaml | 2 +- firebase-dataconnect/emulator/dataconnect/dataconnect.yaml | 2 +- .../google/firebase/dataconnect/proto/connector_service.proto | 2 +- .../google/firebase/dataconnect/proto/graphql_error.proto | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index fa16a7ed32d..b21ac994950 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - +* [changed] Changed gRPC proto package to v1 (was v1beta). + ([#6729](https://github.com/firebase/firebase-android-sdk/pull/6729)) # 16.0.0-beta04 * [changed] `FirebaseDataConnect.logLevel` type changed from `LogLevel` to diff --git a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml index 341a3fc587a..3a718496328 100644 --- a/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml +++ b/firebase-dataconnect/demo/firebase/dataconnect/dataconnect.yaml @@ -1,4 +1,4 @@ -specVersion: v1beta +specVersion: v1 serviceId: srv3ar8skbsza location: us-central1 schema: diff --git a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml index a17c5213bc0..e66af00a793 100644 --- a/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml +++ b/firebase-dataconnect/emulator/dataconnect/dataconnect.yaml @@ -1,4 +1,4 @@ -specVersion: "v1beta" +specVersion: "v1" serviceId: "sid2ehn9ct8te" location: "us-central1" schema: diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto index 918227ef686..bb4bc986769 100644 --- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/connector_service.proto @@ -18,7 +18,7 @@ syntax = "proto3"; -package google.firebase.dataconnect.v1beta; +package google.firebase.dataconnect.v1; import "google/firebase/dataconnect/proto/graphql_error.proto"; import "google/protobuf/struct.proto"; diff --git a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto index f2ca45e9f66..be19dcbfa35 100644 --- a/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto +++ b/firebase-dataconnect/src/main/proto/google/firebase/dataconnect/proto/graphql_error.proto @@ -18,7 +18,7 @@ syntax = "proto3"; -package google.firebase.dataconnect.v1beta; +package google.firebase.dataconnect.v1; import "google/protobuf/struct.proto"; From ad7618bae11e508628ee428e6fcaacbd8a108c80 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:52:30 -0600 Subject: [PATCH 018/162] Add Metalava SemVer Task (#6725) Needs #6724 in main before the task will be able to run --------- Co-authored-by: Rodrigo Lazo --- .github/workflows/metalava-semver-check.yml | 34 ++++++ .../plugins/FirebaseAndroidLibraryPlugin.kt | 8 ++ .../plugins/FirebaseJavaLibraryPlugin.kt | 8 ++ .../firebase/gradle/plugins/Metalava.kt | 1 - .../firebase/gradle/plugins/SemVerTask.kt | 104 ++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/metalava-semver-check.yml create mode 100644 plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt diff --git a/.github/workflows/metalava-semver-check.yml b/.github/workflows/metalava-semver-check.yml new file mode 100644 index 00000000000..df68a691234 --- /dev/null +++ b/.github/workflows/metalava-semver-check.yml @@ -0,0 +1,34 @@ +name: Metalava SemVer Check + +on: + pull_request: + +jobs: + semver-check: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Checkout main + uses: actions/checkout@v4.1.1 + with: + ref: ${{ github.base_ref }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Copy previous api.txt files + run: ./gradlew copyApiTxtFile + + - name: Checkout PR + uses: actions/checkout@v4.1.1 + with: + ref: ${{ github.head_ref }} + clean: false + + - name: Run Metalava SemVer check + run: ./gradlew metalavaSemver diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt index 5ec8dcb22cf..de30763e7b4 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseAndroidLibraryPlugin.kt @@ -165,6 +165,14 @@ class FirebaseAndroidLibraryPlugin : BaseFirebaseLibraryPlugin() { apiTxtFile.set(project.file("api.txt")) output.set(project.file("previous_api.txt")) } + + project.tasks.register("metalavaSemver") { + apiTxtFile.set(project.file("api.txt")) + otherApiFile.set(project.file("previous_api.txt")) + outputApiFile.set(project.file("opi.txt")) + currentVersionString.value(firebaseLibrary.version) + previousVersionString.value(firebaseLibrary.previousVersion) + } } private fun setupApiInformationAnalysis(project: Project, android: LibraryExtension) { diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt index 0068f76e677..acc72d7f0f7 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.kt @@ -108,6 +108,14 @@ class FirebaseJavaLibraryPlugin : BaseFirebaseLibraryPlugin() { apiTxtFile.set(project.file("api.txt")) output.set(project.file("previous_api.txt")) } + + project.tasks.register("metalavaSemver") { + apiTxtFile.set(project.file("api.txt")) + otherApiFile.set(project.file("previous_api.txt")) + outputApiFile.set(project.file("opi.txt")) + currentVersionString.value(firebaseLibrary.version) + previousVersionString.value(firebaseLibrary.previousVersion) + } } private fun setupApiInformationAnalysis(project: Project) { diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt index 4d04701b7f7..233be31a302 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt @@ -55,7 +55,6 @@ fun Project.runMetalavaWithArgs( ) { val allArgs = listOf( - "--no-banner", "--hide", "HiddenSuperclass", // We allow having a hidden parent class "--hide", diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt new file mode 100644 index 00000000000..bafad14816b --- /dev/null +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/SemVerTask.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.gradle.plugins + +import com.google.firebase.gradle.plugins.semver.VersionDelta +import java.io.ByteArrayOutputStream +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class SemVerTask : DefaultTask() { + @get:InputFile abstract val apiTxtFile: RegularFileProperty + @get:InputFile abstract val otherApiFile: RegularFileProperty + @get:Input abstract val currentVersionString: Property + @get:Input abstract val previousVersionString: Property + + @get:OutputFile abstract val outputApiFile: RegularFileProperty + + @TaskAction + fun run() { + val previous = ModuleVersion.fromStringOrNull(previousVersionString.get()) ?: return + val current = ModuleVersion.fromStringOrNull(currentVersionString.get()) ?: return + + val bump = + when { + previous.major != current.major -> VersionDelta.MAJOR + previous.minor != current.minor -> VersionDelta.MINOR + else -> VersionDelta.PATCH + } + val stream = ByteArrayOutputStream() + project.runMetalavaWithArgs( + listOf( + "--source-files", + apiTxtFile.get().asFile.absolutePath, + "--check-compatibility:api:released", + otherApiFile.get().asFile.absolutePath, + ) + + MAJOR.flatMap { m -> listOf("--error", m) } + + MINOR.flatMap { m -> listOf("--error", m) } + + IGNORED.flatMap { m -> listOf("--hide", m) } + + listOf("--format=v3", "--no-color"), + ignoreFailure = true, + stdOut = stream, + ) + + val string = String(stream.toByteArray()) + val reg = Regex("(.*)\\s+error:\\s+(.*\\s+\\[(.*)\\])") + val minorChanges = mutableListOf() + val majorChanges = mutableListOf() + for (match in reg.findAll(string)) { + val loc = match.groups[1]!!.value + val message = match.groups[2]!!.value + val type = match.groups[3]!!.value + if (IGNORED.contains(type)) { + continue // Shouldn't be possible + } else if (MINOR.contains(type)) { + minorChanges.add(message) + } else { + majorChanges.add(message) + } + } + val allChanges = + (majorChanges.joinToString(separator = "") { m -> " MAJOR: $m\n" }) + + minorChanges.joinToString(separator = "") { m -> " MINOR: $m\n" } + if (majorChanges.isNotEmpty()) { + if (bump != VersionDelta.MAJOR) { + throw GradleException( + "API has non-bumped breaking MAJOR changes\nCurrent version bump is ${bump}, update the gradle.properties or fix the changes\n$allChanges" + ) + } + } else if (minorChanges.isNotEmpty()) { + if (bump != VersionDelta.MAJOR && bump != VersionDelta.MINOR) { + throw GradleException( + "API has non-bumped MINOR changes\nCurrent version bump is ${bump}, update the gradle.properties or fix the changes\n$allChanges" + ) + } + } + } + + companion object { + private val MAJOR = setOf("AddedFinal") + private val MINOR = setOf("AddedClass", "AddedMethod", "AddedField", "ChangedDeprecated") + private val IGNORED = setOf("ReferencesDeprecated") + } +} From ec26a52d453101fef3f880ceeb6aba5d1a935808 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Mon, 3 Mar 2025 11:38:48 -0500 Subject: [PATCH 019/162] Use lazy encoding in utf-8 encoded string comparison (#6706) --- firebase-firestore/CHANGELOG.md | 1 + .../firebase/firestore/FirestoreTest.java | 209 ++++++++++++++---- .../google/firebase/firestore/util/Util.java | 41 +++- .../firebase/firestore/util/UtilTest.java | 182 +++++++++++++++ 4 files changed, 390 insertions(+), 43 deletions(-) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index f14e653e79f..939f70c93bb 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [fixed] Use lazy encoding in UTF-8 encoded byte comparison for strings to solve performance issues. [#6706](//github.com/firebase/firebase-android-sdk/pull/6706) * [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java index 796632e192e..95dcd2863fe 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java @@ -1658,17 +1658,33 @@ public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() { public void snapshotListenerSortsUnicodeStringsAsServer() { Map> testDocs = map( - "a", map("value", "Łukasiewicz"), - "b", map("value", "Sierpiński"), - "c", map("value", "岩澤"), - "d", map("value", "🄟"), - "e", map("value", "P"), - "f", map("value", "︒"), - "g", map("value", "🐵")); + "a", + map("value", "Łukasiewicz"), + "b", + map("value", "Sierpiński"), + "c", + map("value", "岩澤"), + "d", + map("value", "🄟"), + "e", + map("value", "P"), + "f", + map("value", "︒"), + "g", + map("value", "🐵"), + "h", + map("value", "你好"), + "i", + map("value", "你顥"), + "j", + map("value", "😁"), + "k", + map("value", "😀")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1699,17 +1715,33 @@ public void snapshotListenerSortsUnicodeStringsAsServer() { public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { Map> testDocs = map( - "a", map("value", Arrays.asList("Łukasiewicz")), - "b", map("value", Arrays.asList("Sierpiński")), - "c", map("value", Arrays.asList("岩澤")), - "d", map("value", Arrays.asList("🄟")), - "e", map("value", Arrays.asList("P")), - "f", map("value", Arrays.asList("︒")), - "g", map("value", Arrays.asList("🐵"))); + "a", + map("value", Arrays.asList("Łukasiewicz")), + "b", + map("value", Arrays.asList("Sierpiński")), + "c", + map("value", Arrays.asList("岩澤")), + "d", + map("value", Arrays.asList("🄟")), + "e", + map("value", Arrays.asList("P")), + "f", + map("value", Arrays.asList("︒")), + "g", + map("value", Arrays.asList("🐵")), + "h", + map("value", Arrays.asList("你好")), + "i", + map("value", Arrays.asList("你顥")), + "j", + map("value", Arrays.asList("😁")), + "k", + map("value", Arrays.asList("😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1740,17 +1772,33 @@ public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { public void snapshotListenerSortsUnicodeStringsInMapAsServer() { Map> testDocs = map( - "a", map("value", map("foo", "Łukasiewicz")), - "b", map("value", map("foo", "Sierpiński")), - "c", map("value", map("foo", "岩澤")), - "d", map("value", map("foo", "🄟")), - "e", map("value", map("foo", "P")), - "f", map("value", map("foo", "︒")), - "g", map("value", map("foo", "🐵"))); + "a", + map("value", map("foo", "Łukasiewicz")), + "b", + map("value", map("foo", "Sierpiński")), + "c", + map("value", map("foo", "岩澤")), + "d", + map("value", map("foo", "🄟")), + "e", + map("value", map("foo", "P")), + "f", + map("value", map("foo", "︒")), + "g", + map("value", map("foo", "🐵")), + "h", + map("value", map("foo", "你好")), + "i", + map("value", map("foo", "你顥")), + "j", + map("value", map("foo", "😁")), + "k", + map("value", map("foo", "😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1781,17 +1829,33 @@ public void snapshotListenerSortsUnicodeStringsInMapAsServer() { public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { Map> testDocs = map( - "a", map("value", map("Łukasiewicz", "foo")), - "b", map("value", map("Sierpiński", "foo")), - "c", map("value", map("岩澤", "foo")), - "d", map("value", map("🄟", "foo")), - "e", map("value", map("P", "foo")), - "f", map("value", map("︒", "foo")), - "g", map("value", map("🐵", "foo"))); + "a", + map("value", map("Łukasiewicz", "foo")), + "b", + map("value", map("Sierpiński", "foo")), + "c", + map("value", map("岩澤", "foo")), + "d", + map("value", map("🄟", "foo")), + "e", + map("value", map("P", "foo")), + "f", + map("value", map("︒", "foo")), + "g", + map("value", map("🐵", "foo")), + "h", + map("value", map("你好", "foo")), + "i", + map("value", map("你顥", "foo")), + "j", + map("value", map("😁", "foo")), + "k", + map("value", map("😀", "foo"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1822,18 +1886,83 @@ public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() { Map> testDocs = map( - "Łukasiewicz", map("value", "foo"), - "Sierpiński", map("value", "foo"), - "岩澤", map("value", "foo"), - "🄟", map("value", "foo"), - "P", map("value", "foo"), - "︒", map("value", "foo"), - "🐵", map("value", "foo")); + "Łukasiewicz", + map("value", "foo"), + "Sierpiński", + map("value", "foo"), + "岩澤", + map("value", "foo"), + "🄟", + map("value", "foo"), + "P", + map("value", "foo"), + "︒", + map("value", "foo"), + "🐵", + map("value", "foo"), + "你好", + map("value", "foo"), + "你顥", + map("value", "foo"), + "😁", + map("value", "foo"), + "😀", + map("value", "foo")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy(FieldPath.documentId()); List expectedDocIds = - Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵"); + Arrays.asList( + "Sierpiński", "Łukasiewicz", "你好", "你顥", "岩澤", "︒", "P", "🄟", "🐵", "😀", "😁"); + + QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); + List getSnapshotDocIds = + getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList()); + + EventAccumulator eventAccumulator = new EventAccumulator(); + ListenerRegistration registration = + orderedQuery.addSnapshotListener(eventAccumulator.listener()); + + List watchSnapshotDocIds = new ArrayList<>(); + try { + QuerySnapshot watchSnapshot = eventAccumulator.await(); + watchSnapshotDocIds = + watchSnapshot.getDocuments().stream() + .map(documentSnapshot -> documentSnapshot.getId()) + .collect(Collectors.toList()); + } finally { + registration.remove(); + } + + assertTrue(getSnapshotDocIds.equals(expectedDocIds)); + assertTrue(watchSnapshotDocIds.equals(expectedDocIds)); + + checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0])); + } + + @Test + public void snapshotListenerSortsInvalidUnicodeStringsAsServer() { + // Note: Protocol Buffer converts any invalid surrogates to "?". + Map> testDocs = + map( + "a", + map("value", "Z"), + "b", + map("value", "你好"), + "c", + map("value", "😀"), + "d", + map("value", "ab\uD800"), // Lone high surrogate + "e", + map("value", "ab\uDC00"), // Lone low surrogate + "f", + map("value", "ab\uD800\uD800"), // Unpaired high surrogate + "g", + map("value", "ab\uDC00\uDC00")); // Unpaired low surrogate + + CollectionReference colRef = testCollectionWithDocs(testDocs); + Query orderedQuery = colRef.orderBy("value"); + List expectedDocIds = Arrays.asList("a", "d", "e", "f", "g", "b", "c"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 543da11e7d3..2cc39337002 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -87,9 +87,44 @@ public static int compareIntegers(int i1, int i2) { /** Compare strings in UTF-8 encoded byte order */ public static int compareUtf8Strings(String left, String right) { - ByteString leftBytes = ByteString.copyFromUtf8(left); - ByteString rightBytes = ByteString.copyFromUtf8(right); - return compareByteStrings(leftBytes, rightBytes); + int i = 0; + while (i < left.length() && i < right.length()) { + int leftCodePoint = left.codePointAt(i); + int rightCodePoint = right.codePointAt(i); + + if (leftCodePoint != rightCodePoint) { + if (leftCodePoint < 128 && rightCodePoint < 128) { + // ASCII comparison + return Integer.compare(leftCodePoint, rightCodePoint); + } else { + // substring and do UTF-8 encoded byte comparison + ByteString leftBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(left, i)); + ByteString rightBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(right, i)); + int comp = compareByteStrings(leftBytes, rightBytes); + if (comp != 0) { + return comp; + } else { + // EXTREMELY RARE CASE: Code points differ, but their UTF-8 byte representations are + // identical. This can happen with malformed input (invalid surrogate pairs), where + // Java's encoding leads to unexpected byte sequences. Meanwhile, any invalid surrogate + // inputs get converted to "?" by protocol buffer while round tripping, so we almost + // never receive invalid strings from backend. + // Fallback to code point comparison for graceful handling. + return Integer.compare(leftCodePoint, rightCodePoint); + } + } + } + // Increment by 2 for surrogate pairs, 1 otherwise. + i += Character.charCount(leftCodePoint); + } + + // Compare lengths if all characters are equal + return Integer.compare(left.length(), right.length()); + } + + private static String getUtf8SafeBytes(String str, int index) { + int firstCodePoint = str.codePointAt(index); + return str.substring(index, index + Character.charCount(firstCodePoint)); } /** diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java index 6ff424ef994..ccd88854ba7 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.firestore.util.Util.firstNEntries; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.google.firebase.firestore.testutil.TestUtil; import com.google.protobuf.ByteString; @@ -26,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -87,4 +89,184 @@ private void validateDiffCollection(List before, List after) { Util.diffCollections(before, after, String::compareTo, result::add, result::remove); assertThat(result).containsExactlyElementsIn(after); } + + @Test + public void compareUtf8StringsShouldReturnCorrectValue() { + ArrayList errors = new ArrayList<>(); + int seed = new Random().nextInt(Integer.MAX_VALUE); + int passCount = 0; + StringGenerator stringGenerator = new StringGenerator(29750468); + StringPairGenerator stringPairGenerator = new StringPairGenerator(stringGenerator); + for (int i = 0; i < 1_000_000 && errors.size() < 10; i++) { + StringPairGenerator.StringPair stringPair = stringPairGenerator.next(); + final String s1 = stringPair.s1; + final String s2 = stringPair.s2; + + int actual = Util.compareUtf8Strings(s1, s2); + + ByteString b1 = ByteString.copyFromUtf8(s1); + ByteString b2 = ByteString.copyFromUtf8(s2); + int expected = Util.compareByteStrings(b1, b2); + + if (actual == expected) { + passCount++; + } else { + errors.add( + "compareUtf8Strings(s1=\"" + + s1 + + "\", s2=\"" + + s2 + + "\") returned " + + actual + + ", but expected " + + expected + + " (i=" + + i + + ", s1.length=" + + s1.length() + + ", s2.length=" + + s2.length() + + ")"); + } + } + + if (!errors.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(errors.size()).append(" test cases failed, "); + sb.append(passCount).append(" test cases passed, "); + sb.append("seed=").append(seed).append(";"); + for (int i = 0; i < errors.size(); i++) { + sb.append("\nerrors[").append(i).append("]: ").append(errors.get(i)); + } + fail(sb.toString()); + } + } + + private static class StringPairGenerator { + + private final StringGenerator stringGenerator; + + public StringPairGenerator(StringGenerator stringGenerator) { + this.stringGenerator = stringGenerator; + } + + public StringPair next() { + String prefix = stringGenerator.next(); + String s1 = prefix + stringGenerator.next(); + String s2 = prefix + stringGenerator.next(); + return new StringPair(s1, s2); + } + + public static class StringPair { + public final String s1, s2; + + public StringPair(String s1, String s2) { + this.s1 = s1; + this.s2 = s2; + } + } + } + + private static class StringGenerator { + + private static final float DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33f; + private static final int DEFAULT_MAX_LENGTH = 20; + + private static final int MIN_HIGH_SURROGATE = 0xD800; + private static final int MAX_HIGH_SURROGATE = 0xDBFF; + private static final int MIN_LOW_SURROGATE = 0xDC00; + private static final int MAX_LOW_SURROGATE = 0xDFFF; + + private final Random rnd; + private final float surrogatePairProbability; + private final int maxLength; + + public StringGenerator(int seed) { + this(new Random(seed), DEFAULT_SURROGATE_PAIR_PROBABILITY, DEFAULT_MAX_LENGTH); + } + + public StringGenerator(Random rnd, float surrogatePairProbability, int maxLength) { + this.rnd = rnd; + this.surrogatePairProbability = validateProbability(surrogatePairProbability); + this.maxLength = validateLength(maxLength); + } + + private static float validateProbability(float probability) { + if (!Float.isFinite(probability)) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be between 0.0 and 1.0, inclusive)"); + } else if (probability < 0.0f) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be greater than or equal to zero)"); + } else if (probability > 1.0f) { + throw new IllegalArgumentException( + "invalid surrogate pair probability: " + + probability + + " (must be less than or equal to 1)"); + } + return probability; + } + + private static int validateLength(int length) { + if (length < 0) { + throw new IllegalArgumentException( + "invalid maximum string length: " + + length + + " (must be greater than or equal to zero)"); + } + return length; + } + + public String next() { + final int length = rnd.nextInt(maxLength + 1); + final StringBuilder sb = new StringBuilder(); + while (sb.length() < length) { + int codePoint = nextCodePoint(); + sb.appendCodePoint(codePoint); + } + return sb.toString(); + } + + private boolean isNextSurrogatePair() { + return nextBoolean(rnd, surrogatePairProbability); + } + + private static boolean nextBoolean(Random rnd, float probability) { + if (probability == 0.0f) { + return false; + } else if (probability == 1.0f) { + return true; + } else { + return rnd.nextFloat() < probability; + } + } + + private int nextCodePoint() { + if (isNextSurrogatePair()) { + return nextSurrogateCodePoint(); + } else { + return nextNonSurrogateCodePoint(); + } + } + + private int nextSurrogateCodePoint() { + int highSurrogate = + rnd.nextInt(MAX_HIGH_SURROGATE - MIN_HIGH_SURROGATE + 1) + MIN_HIGH_SURROGATE; + int lowSurrogate = rnd.nextInt(MAX_LOW_SURROGATE - MIN_LOW_SURROGATE + 1) + MIN_LOW_SURROGATE; + return Character.toCodePoint((char) highSurrogate, (char) lowSurrogate); + } + + private int nextNonSurrogateCodePoint() { + int codePoint; + do { + codePoint = rnd.nextInt(0x10000); // BMP range + } while (codePoint >= MIN_HIGH_SURROGATE + && codePoint <= MAX_LOW_SURROGATE); // Exclude surrogate range + return codePoint; + } + } } From d701c3437419644dc7846e59613c87dcf070976a Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 3 Mar 2025 13:46:33 -0700 Subject: [PATCH 020/162] Avoid Process.myProcessName() on Android 13 (#6720) Avoid calling `Process.myProcessName()` on Android 13 because it appears to be missing from some OEM-specific Android 13 builds. It is fine to just let the method fall through to the next, older, method to get the process name. See https://github.com/firebase/firebase-unity-sdk/issues/1059 I have not been able to reproduce this issue locally, but this change is very safe. We should consider refactoring Crashlytics to consume the Sessions `ProcessDetails` data class, instead of the current `@AutoValue` holder. --- firebase-crashlytics/CHANGELOG.md | 13 +++++++++++-- .../crashlytics/internal/ProcessDetailsProvider.kt | 6 ++++-- .../firebase/sessions/ProcessDetailsProvider.kt | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index f0bbb91128b..a6e5e087b60 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,6 +1,15 @@ # Unreleased +* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720] + +# 19.4.1 * [changed] Updated `firebase-sessions` dependency to v2.0.9 + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + # 19.4.0 * [feature] Added an overload for `recordException` that allows logging additional custom keys to the non fatal event [#3551] @@ -324,10 +333,10 @@ updates. # 18.2.10 * [fixed] Fixed a bug that could prevent unhandled exceptions from being - propogated to the default handler when the network is unavailable. + propagated to the default handler when the network is unavailable. * [changed] Internal changes to support on-demand fatal crash reporting for Flutter apps. -* [fixed] Fixed a bug that prevented [crashlytics] from initalizing on some +* [fixed] Fixed a bug that prevented [crashlytics] from initializing on some devices in some cases. (#3269) diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt index 49fd2fafd18..172ebaaf477 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/ProcessDetailsProvider.kt @@ -29,6 +29,8 @@ import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.Session. * @hide */ internal object ProcessDetailsProvider { + // TODO(mrober): Merge this with [com.google.firebase.sessions.ProcessDetailsProvider]. + /** Gets the details for all of this app's running processes. */ fun getAppProcessDetails(context: Context): List { val appUid = context.applicationInfo.uid @@ -70,7 +72,7 @@ internal object ProcessDetailsProvider { processName: String, pid: Int = 0, importance: Int = 0, - isDefaultProcess: Boolean = false + isDefaultProcess: Boolean = false, ) = ProcessDetails.builder() .setProcessName(processName) @@ -81,7 +83,7 @@ internal object ProcessDetailsProvider { /** Gets the app's current process name. If the API is not available, returns an empty string. */ private fun getProcessName(): String = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { Process.myProcessName() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { Application.getProcessName() ?: "" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt index 72e80469880..65d1dfbbc60 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/ProcessDetailsProvider.kt @@ -74,7 +74,7 @@ internal object ProcessDetailsProvider { /** Gets the app's current process name. If it could not be found, returns an empty string. */ internal fun getProcessName(): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { return Process.myProcessName() } From 79deb5f2fd600cab1f71f30ae808865ee7909e42 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 3 Mar 2025 13:46:42 -0700 Subject: [PATCH 021/162] Fix bug that let responsePayloadBytes get set to -1 (#6721) Fix a bug in `InstrHttpInputStream` that let `NetworkRequestMetric.responsePayloadBytes` get set to -1 in some conditions. While investigating [b/398063523](http://b/398063523), I found that `inputStream.read(...)` can return 0 in some cases, for example, when the byte buffer length is 0. When this happens, it was possible to set `responsePayloadBytes` to -1 because `-1 + 0 = -1`. I didn't just have `bytesRead` initialize to 0 because there is a difference between 0 bytes read, and no read happened. Tested manually by hacking a test app to force this to happen, and by unit tests. --- firebase-perf/CHANGELOG.md | 1 + .../perf/network/InstrHttpInputStream.java | 37 +++++++++++-------- .../network/InstrHttpInputStreamTest.java | 34 ++++++++++++++--- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 58bdd0f1d29..9cfa4e6537a 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased * [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] +* [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics. # 21.0.4 diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java index fc660c70426..5ffff6c0d2f 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/network/InstrHttpInputStream.java @@ -30,13 +30,7 @@ public final class InstrHttpInputStream extends InputStream { private long timeToResponseInitiated; private long timeToResponseLastRead = -1; - /** - * Instrumented inputStream object - * - * @param inputStream - * @param builder - * @param timer - */ + /** Instrumented inputStream object */ public InstrHttpInputStream( final InputStream inputStream, final NetworkRequestMetricBuilder builder, Timer timer) { this.timer = timer; @@ -99,12 +93,13 @@ public int read() throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead++; + incrementBytesRead(1); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -124,12 +119,13 @@ public int read(final byte[] buffer, final int byteOffset, final int byteCount) if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead += bytesRead; + incrementBytesRead(bytesRead); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -148,12 +144,13 @@ public int read(final byte[] buffer) throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (bytesRead == -1 && timeToResponseLastRead == -1) { + boolean endOfStream = bytesRead == -1; + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); networkMetricBuilder.build(); } else { - this.bytesRead += bytesRead; + incrementBytesRead(bytesRead); networkMetricBuilder.setResponsePayloadBytes(this.bytesRead); } return bytesRead; @@ -183,11 +180,13 @@ public long skip(final long byteCount) throws IOException { if (timeToResponseInitiated == -1) { timeToResponseInitiated = tempTime; } - if (skipped == -1 && timeToResponseLastRead == -1) { + // InputStream.skip will return 0 for both end of stream and for 0 bytes skipped. + boolean endOfStream = (skipped == 0 && byteCount != 0); + if (endOfStream && timeToResponseLastRead == -1) { timeToResponseLastRead = tempTime; networkMetricBuilder.setTimeToResponseCompletedMicros(timeToResponseLastRead); } else { - bytesRead += skipped; + incrementBytesRead(skipped); networkMetricBuilder.setResponsePayloadBytes(bytesRead); } return skipped; @@ -197,4 +196,12 @@ public long skip(final long byteCount) throws IOException { throw e; } } + + private void incrementBytesRead(long bytesRead) { + if (this.bytesRead == -1) { + this.bytesRead = bytesRead; + } else { + this.bytesRead += bytesRead; + } + } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java index 8a7ecb2131b..e1f45c45329 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/network/InstrHttpInputStreamTest.java @@ -30,6 +30,7 @@ import com.google.firebase.perf.v1.NetworkRequestMetric.NetworkClientErrorReason; import java.io.IOException; import java.io.InputStream; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,10 +41,14 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -/** Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}. */ +/** + * Unit tests for {@link com.google.firebase.perf.network.InstrHttpInputStream}. + * + * @noinspection ResultOfMethodCallIgnored + */ @RunWith(RobolectricTestRunner.class) public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase { - + private AutoCloseable closeable; @Mock InputStream mInputStream; @Mock TransportManager transportManager; @Mock Timer timer; @@ -53,12 +58,17 @@ public class InstrHttpInputStreamTest extends FirebasePerformanceTestBase { @Before public void setUp() { - MockitoAnnotations.initMocks(this); + closeable = MockitoAnnotations.openMocks(this); when(timer.getMicros()).thenReturn((long) 1000); when(timer.getDurationMicros()).thenReturn((long) 2000); networkMetricBuilder = NetworkRequestMetricBuilder.builder(transportManager); } + @After + public void releaseMocks() throws Exception { + closeable.close(); + } + @Test public void testAvailable() throws IOException { int availableVal = 7; @@ -80,7 +90,7 @@ public void testClose() throws IOException { } @Test - public void testMark() throws IOException { + public void testMark() { int markInput = 256; new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).mark(markInput); @@ -89,7 +99,7 @@ public void testMark() throws IOException { } @Test - public void testMarkSupported() throws IOException { + public void testMarkSupported() { when(mInputStream.markSupported()).thenReturn(true); boolean ret = new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).markSupported(); @@ -108,6 +118,20 @@ public void testRead() throws IOException { verify(mInputStream).read(); } + @Test + public void testReadBufferOffsetZero() throws IOException { + byte[] b = new byte[0]; + int off = 0; + int len = 0; + when(mInputStream.read(b, off, len)).thenReturn(len); + int ret = new InstrHttpInputStream(mInputStream, networkMetricBuilder, timer).read(b, off, len); + + NetworkRequestMetric metric = networkMetricBuilder.build(); + assertThat(ret).isEqualTo(0); + assertThat(metric.getResponsePayloadBytes()).isEqualTo(0); + verify(mInputStream).read(b, off, len); + } + @Test public void testReadBufferOffsetCount() throws IOException { byte[] buffer = new byte[] {(byte) 0xe0}; From 92632af12a44ac26d177ff5b967b7f2f0fe10f5f Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 4 Mar 2025 06:47:40 -0700 Subject: [PATCH 022/162] Update datastore dependency to 1.1.3 (#6688) Update datastore dependency to `1.1.3` to address [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8) in AQS. We had landed #6343, but it missed the datastore dependency because version 1.0.0 "shaded" the vulnerable protobuf dependency, see #6534. I verified this was happening by extracting the jar from https://maven.google.com/web/index.html?q=datastore-pre#androidx.datastore:datastore-preferences-core:1.0.0 and seeing `com.google.protobufprotobuf-parent3.10.0` nested in a maven dir. I also verified datastore 1.1.3 has upgraded the protobuf version to 4.28.2, a safe version. See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-datastore-release:gradle/libs.versions.toml;l=59. This datastore update also includes the stable `MultiProcessDataStoreFactory` which we can utilize in a future change to optimize things like the settings fetch for multi-process apps. --- firebase-sessions/CHANGELOG.md | 4 ++++ firebase-sessions/firebase-sessions.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ smoke-tests/build.gradle | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 48987a62df5..2473b64a1cf 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +* [changed] Updated datastore dependency to `1.1.3` to + fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). + +# 2.0.9 * [fixed] Make AQS resilient to background init in multi-process apps. # 2.0.7 diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts index 15d22381e31..0a09740bd77 100644 --- a/firebase-sessions/firebase-sessions.gradle.kts +++ b/firebase-sessions/firebase-sessions.gradle.kts @@ -67,12 +67,12 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-common") exclude(group = "com.google.firebase", module = "firebase-components") } - implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("com.google.android.datatransport:transport-api:3.2.0") api("com.google.firebase:firebase-annotations:16.2.0") api("com.google.firebase:firebase-encoders:17.0.0") api("com.google.firebase:firebase-encoders-json:18.0.1") implementation(libs.androidx.annotation) + implementation(libs.androidx.datastore.preferences) compileOnly(libs.errorprone.annotations) runtimeOnly("com.google.firebase:firebase-installations:18.0.0") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b234d7bbd0f..4881c9d7d40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ constraintlayout = "2.1.4" coreKtx = "1.12.0" coroutines = "1.7.3" dagger = "2.43.2" +datastore = "1.1.3" dexmaker = "2.28.1" dexmakerVersion = "1.2" espressoCore = "3.6.1" @@ -91,6 +92,7 @@ androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "card androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core = { module = "androidx.core:core", version = "1.2.0" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } androidx-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "espressoCore" } androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoCore" } diff --git a/smoke-tests/build.gradle b/smoke-tests/build.gradle index 346bad8698f..89df856dd06 100644 --- a/smoke-tests/build.gradle +++ b/smoke-tests/build.gradle @@ -24,12 +24,14 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:8.3.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0" classpath "com.google.gms:google-services:4.3.14" classpath "com.google.firebase:firebase-crashlytics-gradle:2.8.1" } } apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" android { compileSdkVersion 34 From 1aaa6cd240de6797889e47bf14a95959dd9675ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:29:44 -0500 Subject: [PATCH 023/162] build(deps): bump org.eclipse.jgit:org.eclipse.jgit from 6.3.0.202209071007-r to 7.1.0.202411261347-r (#6733) Bumps [org.eclipse.jgit:org.eclipse.jgit](https://github.com/eclipse-jgit/jgit) from 6.3.0.202209071007-r to 7.1.0.202411261347-r.
Commits
  • 4d1d885 JGit v7.1.0.202411261347-r
  • 856c1c3 Merge branch 'master' into stable-7.1
  • 683d444 Merge branch 'stable-7.0'
  • e3eabe5 Merge branch 'stable-6.10' into stable-7.0
  • f27ea51 Merge "Pack.java: Recover more often in Pack.copyAsIs2()" into stable-6.10
  • f026c19 PackDirectory: Filter out tmp GC pack files
  • 6fa28d7 Add pack-refs command to the CLI
  • 079dbe8 Test advertised capabilities with protocol V0 and allow*Sha1InWant
  • 5b1513a Align request policies with CGit
  • f295477 Merge "GitTimeParser: Fix multiple errorprone and style comments"
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.eclipse.jgit:org.eclipse.jgit&package-manager=gradle&previous-version=6.3.0.202209071007-r&new-version=7.1.0.202411261347-r)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo --- plugins/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index 85bad8507be..f429250bbc4 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -61,7 +61,7 @@ dependencies { implementation("com.google.guava:guava:31.1-jre") implementation("org.ow2.asm:asm-tree:9.5") - implementation("org.eclipse.jgit:org.eclipse.jgit:6.3.0.202209071007-r") + implementation("org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r") implementation(libs.kotlinx.serialization.json) implementation("com.google.code.gson:gson:2.8.9") implementation(libs.android.gradlePlugin.gradle) From e12597e04f7f607518ecd37727a9bd280b941b33 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Wed, 5 Mar 2025 16:16:09 -0500 Subject: [PATCH 024/162] build(deps): bump kotestAssertionsCore from 5.5.5 to 5.8.1 (#6736) Last version depending on kotlin stdlib 1.8.x --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4881c9d7d40..489b504e75c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ jacksonDatabind = "2.18.2" javalite = "3.25.5" jsonassert = "1.5.0" kotest = "5.9.0" # Do not use 5.9.1 because it reverts the fix for https://github.com/kotest/kotest/issues/3981 -kotestAssertionsCore = "5.5.5" +kotestAssertionsCore = "5.8.1" kotlin = "1.8.22" ktorVersion = "2.3.2" legacySupportV4 = "1.0.0" From 4128d9a71f3b827f0c7314a1c7c6b46215b3da83 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Thu, 6 Mar 2025 19:17:14 -0500 Subject: [PATCH 025/162] Remove unnecessary steps in CI Testing for vertex (#6743) Additionally, make the update_responses.sh more verbose to ease debugging --- .github/workflows/ci_tests.yml | 11 +---------- firebase-vertexai/update_responses.sh | 2 ++ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index c706aa614bd..603719e8142 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -56,18 +56,9 @@ jobs: distribution: temurin cache: gradle - - name: Pull genai-common - if: matrix.module == ':firebase-vertexai' - run: | - git clone https://github.com/google-gemini/generative-ai-android.git - cd generative-ai-android - ./gradlew :common:updateVersion common:publishToMavenLocal - cd .. - - name: Clone mock responses if: matrix.module == ':firebase-vertexai' - run: | - firebase-vertexai/update_responses.sh + run: firebase-vertexai/update_responses.sh - name: Add google-services.json env: diff --git a/firebase-vertexai/update_responses.sh b/firebase-vertexai/update_responses.sh index 70e438090bd..3feec3b861b 100755 --- a/firebase-vertexai/update_responses.sh +++ b/firebase-vertexai/update_responses.sh @@ -21,6 +21,8 @@ RESPONSES_VERSION='v6.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" +set -x + cd "$(dirname "$0")/src/test/resources" || exit rm -rf "$REPO_NAME" git clone "$REPO_LINK" --quiet || exit From a232b6d467b5fb51aa9a433234c7b471e7ce2649 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 10 Mar 2025 12:31:34 -0400 Subject: [PATCH 026/162] Update functions changelog (#6751) The mergeback PR from last release didn't include the update to function's CHANGELOG.md file --- firebase-functions/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index e9fe66c897d..26bb8de7306 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,8 +1,15 @@ # Unreleased + +# 21.1.1 * [fixed] Resolve Kotlin migration visibility issues ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522)) +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. + # 21.1.0 * [changed] Migrated to Kotlin From 123c9d7b58258b2b7760d69ed35e7fccf97cb557 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 10 Mar 2025 13:33:40 -0400 Subject: [PATCH 027/162] [VertexAI] Add initial support to export covered API (#6749) The server produces a discovery document with the details of the API surface. https://aiplatform.googleapis.com/$discovery/rest?version=v1beta1 This change introduces code that can generate similar a description of the API covered by the SDK. This will enable us to track difference between both. In a follow up PR we can implement the logic to fully export the surface. --------- Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com> --- .../vertexai/common/util/serialization.kt | 8 +- .../firebase/vertexai/SerializationTests.kt | 215 ++++++++++++++++++ .../vertexai/common/util/descriptorToJson.kt | 167 ++++++++++++++ 3 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt create mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt index e64bb89afc3..4a2570b82d6 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/serialization.kt @@ -23,6 +23,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @@ -36,7 +37,12 @@ import kotlinx.serialization.encoding.Encoder */ internal class FirstOrdinalSerializer>(private val enumClass: KClass) : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FirstOrdinalSerializer") + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("FirstOrdinalSerializer") { + for (enumValue in enumClass.enumValues()) { + element(enumValue.toString()) + } + } override fun deserialize(decoder: Decoder): T { val name = decoder.decodeString() diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt new file mode 100644 index 00000000000..cf6a40680e5 --- /dev/null +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/SerializationTests.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai + +import com.google.firebase.vertexai.common.util.descriptorToJson +import com.google.firebase.vertexai.type.Candidate +import com.google.firebase.vertexai.type.CountTokensResponse +import com.google.firebase.vertexai.type.GenerateContentResponse +import com.google.firebase.vertexai.type.ModalityTokenCount +import com.google.firebase.vertexai.type.Schema +import io.kotest.assertions.json.shouldEqualJson +import org.junit.Test + +internal class SerializationTests { + @Test + fun `test countTokensResponse serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "CountTokensResponse", + "type": "object", + "properties": { + "totalTokens": { + "type": "integer" + }, + "totalBillableCharacters": { + "type": "integer" + }, + "promptTokensDetails": { + "type": "array", + "items": { + "${'$'}ref": "ModalityTokenCount" + } + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(CountTokensResponse.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test modalityTokenCount serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "ModalityTokenCount", + "type": "object", + "properties": { + "modality": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "TEXT", + "IMAGE", + "VIDEO", + "AUDIO", + "DOCUMENT" + ] + }, + "tokenCount": { + "type": "integer" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(ModalityTokenCount.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GenerateContentResponse serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GenerateContentResponse", + "type": "object", + "properties": { + "candidates": { + "type": "array", + "items": { + "${'$'}ref": "Candidate" + } + }, + "promptFeedback": { + "${'$'}ref": "PromptFeedback" + }, + "usageMetadata": { + "${'$'}ref": "UsageMetadata" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GenerateContentResponse.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Candidate serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "Candidate", + "type": "object", + "properties": { + "content": { + "${'$'}ref": "Content" + }, + "finishReason": { + "type": "string", + "enum": [ + "UNKNOWN", + "UNSPECIFIED", + "STOP", + "MAX_TOKENS", + "SAFETY", + "RECITATION", + "OTHER", + "BLOCKLIST", + "PROHIBITED_CONTENT", + "SPII", + "MALFORMED_FUNCTION_CALL" + ] + }, + "safetyRatings": { + "type": "array", + "items": { + "${'$'}ref": "SafetyRating" + } + }, + "citationMetadata": { + "${'$'}ref": "CitationMetadata" + }, + "groundingMetadata": { + "${'$'}ref": "GroundingMetadata" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Candidate.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Schema serialization as Json`() { + /** + * Unlike the actual schema in the background, we don't represent "type" as an enum, but rather + * as a string. This is because we restrict what values can be used (using helper methods, + * rather than type). + */ + val expectedJsonAsString = + """ + { + "id": "Schema", + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "format": { + "type": "string" + }, + "description": { + "type": "string" + }, + "nullable": { + "type": "boolean" + }, + "items": { + "${'$'}ref": "Schema" + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "${'$'}ref": "Schema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Schema.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } +} diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt new file mode 100644 index 00000000000..31d9156bc75 --- /dev/null +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/descriptorToJson.kt @@ -0,0 +1,167 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.common.util + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.SerialKind +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.descriptors.elementDescriptors +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +/** + * Returns a [JsonObject] representing the classes in the hierarchy of a serialization [descriptor]. + * + * The format of the JSON object is similar to that of a Discovery Document, but restricted to these + * fields: + * - id + * - type + * - properties + * - items + * - $ref + * + * @param descriptor The [SerialDescriptor] to process. + */ +@OptIn(ExperimentalSerializationApi::class) +internal fun descriptorToJson(descriptor: SerialDescriptor): JsonObject { + return buildJsonObject { + put("id", simpleNameFromSerialName(descriptor.serialName)) + put("type", typeNameFromKind(descriptor.kind)) + if (descriptor.kind != StructureKind.CLASS) { + throw UnsupportedOperationException("Only classes can be serialized to JSON for now.") + } + // For top-level enums, add them directly. + if (descriptor.serialName == "FirstOrdinalSerializer") { + addEnumDescription(descriptor) + } else { + addObjectProperties(descriptor) + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addListDescription(descriptor: SerialDescriptor) = + putJsonObject("items") { + val itemDescriptor = descriptor.elementDescriptors.first() + val nestedIsPrimitive = (descriptor.elementsCount == 1 && itemDescriptor.kind is PrimitiveKind) + if (nestedIsPrimitive) { + put("type", typeNameFromKind(itemDescriptor.kind)) + } else { + put("\$ref", simpleNameFromSerialName(itemDescriptor.serialName)) + } + } + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addEnumDescription(descriptor: SerialDescriptor): JsonElement? { + put("type", typeNameFromKind(SerialKind.ENUM)) + return put("enum", JsonArray(descriptor.elementNames.map { JsonPrimitive(it) })) +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun JsonObjectBuilder.addObjectProperties(descriptor: SerialDescriptor): JsonElement? { + return putJsonObject("properties") { + for (i in 0 until descriptor.elementsCount) { + val elementDescriptor = descriptor.getElementDescriptor(i) + val elementName = descriptor.getElementName(i) + putJsonObject(elementName) { + when (elementDescriptor.kind) { + StructureKind.LIST -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + addListDescription(elementDescriptor) + } + StructureKind.CLASS -> { + if (elementDescriptor.serialName.startsWith("FirstOrdinalSerializer")) { + addEnumDescription(elementDescriptor) + } else { + put("\$ref", simpleNameFromSerialName(elementDescriptor.serialName)) + } + } + StructureKind.MAP -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + putJsonObject("additionalProperties") { + put( + "\$ref", + simpleNameFromSerialName(elementDescriptor.getElementDescriptor(1).serialName) + ) + } + } + else -> { + put("type", typeNameFromKind(elementDescriptor.kind)) + } + } + } + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +internal fun typeNameFromKind(kind: SerialKind): String { + return when (kind) { + PrimitiveKind.BOOLEAN -> "boolean" + PrimitiveKind.BYTE -> "integer" + PrimitiveKind.CHAR -> "string" + PrimitiveKind.DOUBLE -> "number" + PrimitiveKind.FLOAT -> "number" + PrimitiveKind.INT -> "integer" + PrimitiveKind.LONG -> "integer" + PrimitiveKind.SHORT -> "integer" + PrimitiveKind.STRING -> "string" + StructureKind.CLASS -> "object" + StructureKind.LIST -> "array" + SerialKind.ENUM -> "string" + StructureKind.MAP -> "object" + /* Only add new cases if they show up in actual test scenarios. */ + else -> TODO() + } +} + +/** + * Extracts the name expected for a class from its serial name. + * + * Our serialization classes are nested within the public-facing classes, and that's the name we + * want in the json output. There are two class names + * + * - `com.google.firebase.vertexai.type.Content.Internal` for regular scenarios + * - `com.google.firebase.vertexai.type.Content.Internal.SomeClass` for nested classes in the + * serializer. + * + * For the later time we need the second to last component, for the former we need the last + * component. + * + * Additionally, given that types can be nullable, we need to strip the `?` from the end of the + * name. + */ +internal fun simpleNameFromSerialName(serialName: String): String = + serialName + .split(".") + .let { + if (it.last().startsWith("Internal")) { + it[it.size - 2] + } else { + it.last() + } + } + .replace("?", "") From aa2bab8225185e43990e87b9f4dc26dd631c66f0 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 10 Mar 2025 11:48:17 -0600 Subject: [PATCH 028/162] Remove code style from deprecated message (#6753) --- .../java/com/google/firebase/crashlytics/KeyValueBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt index 636b975ab1d..74d3793e215 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/KeyValueBuilder.kt @@ -23,7 +23,7 @@ private constructor( private val builder: CustomKeysAndValues.Builder, ) { @Deprecated( - "Do not construct this directly. Use [setCustomKeys] instead. To be removed in the next major release." + "Do not construct this directly. Use `setCustomKeys` instead. To be removed in the next major release." ) constructor(crashlytics: FirebaseCrashlytics) : this(crashlytics, CustomKeysAndValues.Builder()) From af5fd66d31cad06a3977ef7c89ba0bdd6891ccd3 Mon Sep 17 00:00:00 2001 From: welishr <65972773+welishr@users.noreply.github.com> Date: Mon, 10 Mar 2025 15:56:32 -0400 Subject: [PATCH 029/162] Store registered context for sync task unregistrations (#6752) For issue #6558, this is an attempt at fixing the IllegalArgumentException by ensuring that the context we use for registering the SyncTask is the same context we use to unregister the task. Race conditions dont seem like a culprit here since unregister is only triggered by the Receiver itself, which should be only executed synchronously on the main thread. --- firebase-messaging/CHANGELOG.md | 2 ++ .../java/com/google/firebase/messaging/SyncTask.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index 4a7e28a5766..6b0a8bdadd9 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [changed] Bug fix in SyncTask to always unregister the receiver on the same + context on which it was registered. # 24.1.0 diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java index c0c4074c11e..cd821f0e1f3 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java @@ -161,6 +161,7 @@ boolean isDeviceConnected() { static class ConnectivityChangeReceiver extends BroadcastReceiver { @Nullable private SyncTask task; // task is set to null after it has been fired. + @Nullable private Context receiverContext; public ConnectivityChangeReceiver(SyncTask task) { this.task = task; @@ -171,7 +172,10 @@ public void registerReceiver() { Log.d(TAG, "Connectivity change received registered"); } IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - task.getContext().registerReceiver(this, intentFilter); + if (task != null) { + receiverContext = task.getContext(); + receiverContext.registerReceiver(this, intentFilter); + } } @Override @@ -191,7 +195,9 @@ public void onReceive(Context context, Intent intent) { Log.d(TAG, "Connectivity changed. Starting background sync."); } task.firebaseMessaging.enqueueTaskWithDelaySeconds(task, 0); - task.getContext().unregisterReceiver(this); + if (receiverContext != null) { + receiverContext.unregisterReceiver(this); + } task = null; } } From 7c03f4964c48ac0a8f82293e296fa0d6efe42266 Mon Sep 17 00:00:00 2001 From: mustafa jadid Date: Mon, 10 Mar 2025 13:31:17 -0700 Subject: [PATCH 030/162] Extend Firebase SDK with new APIs to consume streaming callable function response (#6602) Extend Firebase SDK with new APIs to consume streaming callable function response. - Handling the server-sent event (SSE) parsing internally - Providing proper error handling and connection management - Maintaining memory efficiency for long-running streams --------- Co-authored-by: Rodrigo Lazo --- firebase-functions/api.txt | 17 + .../firebase-functions.gradle.kts | 3 + .../androidTest/backend/functions/index.js | 107 ++++++ .../google/firebase/functions/StreamTests.kt | 218 ++++++++++++ .../firebase/functions/FirebaseFunctions.kt | 16 + .../functions/HttpsCallableReference.kt | 56 ++- .../firebase/functions/PublisherStream.kt | 328 ++++++++++++++++++ .../firebase/functions/StreamResponse.kt | 57 +++ 8 files changed, 798 insertions(+), 4 deletions(-) create mode 100644 firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt diff --git a/firebase-functions/api.txt b/firebase-functions/api.txt index a9a05c703a8..1a12a250b35 100644 --- a/firebase-functions/api.txt +++ b/firebase-functions/api.txt @@ -84,6 +84,8 @@ package com.google.firebase.functions { method public com.google.android.gms.tasks.Task call(Object? data); method public long getTimeout(); method public void setTimeout(long timeout, java.util.concurrent.TimeUnit units); + method public org.reactivestreams.Publisher stream(); + method public org.reactivestreams.Publisher stream(Object? data = null); method public com.google.firebase.functions.HttpsCallableReference withTimeout(long timeout, java.util.concurrent.TimeUnit units); property public final long timeout; } @@ -93,6 +95,21 @@ package com.google.firebase.functions { field public final Object? data; } + public abstract class StreamResponse { + } + + public static final class StreamResponse.Message extends com.google.firebase.functions.StreamResponse { + ctor public StreamResponse.Message(com.google.firebase.functions.HttpsCallableResult message); + method public com.google.firebase.functions.HttpsCallableResult getMessage(); + property public final com.google.firebase.functions.HttpsCallableResult message; + } + + public static final class StreamResponse.Result extends com.google.firebase.functions.StreamResponse { + ctor public StreamResponse.Result(com.google.firebase.functions.HttpsCallableResult result); + method public com.google.firebase.functions.HttpsCallableResult getResult(); + property public final com.google.firebase.functions.HttpsCallableResult result; + } + } package com.google.firebase.functions.ktx { diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 7ec958bdd79..08a797112b9 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -112,6 +112,8 @@ dependencies { implementation(libs.okhttp) implementation(libs.playservices.base) implementation(libs.playservices.basement) + implementation(libs.reactive.streams) + api(libs.playservices.tasks) kapt(libs.autovalue) @@ -131,6 +133,7 @@ dependencies { androidTestImplementation(libs.truth) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.kotlinx.coroutines.reactive) androidTestImplementation(libs.mockito.core) androidTestImplementation(libs.mockito.dexmaker) kapt("com.google.dagger:dagger-android-processor:2.43.2") diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js index fed5a371b89..f26d6615d68 100644 --- a/firebase-functions/src/androidTest/backend/functions/index.js +++ b/firebase-functions/src/androidTest/backend/functions/index.js @@ -14,6 +14,16 @@ const assert = require('assert'); const functions = require('firebase-functions'); +const functionsV2 = require('firebase-functions/v2'); + +/** + * Pauses the execution for a specified amount of time. + * @param {number} ms - The number of milliseconds to sleep. + * @return {Promise} A promise that resolves after the specified time. + */ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} exports.dataTest = functions.https.onRequest((request, response) => { assert.deepEqual(request.body, { @@ -122,3 +132,100 @@ exports.timeoutTest = functions.https.onRequest((request, response) => { // Wait for longer than 500ms. setTimeout(() => response.send({data: true}), 500); }); + +const streamData = ['hello', 'world', 'this', 'is', 'cool']; + +/** + * Generates chunks of text asynchronously, yielding one chunk at a time. + * @async + * @generator + * @yields {string} A chunk of text from the data array. + */ +async function* generateText() { + for (const chunk of streamData) { + yield chunk; + await sleep(100); + } +} + +exports.genStream = functionsV2.https.onCall(async (request, response) => { + if (request.acceptsStreaming) { + for await (const chunk of generateText()) { + response.sendChunk(chunk); + } + } + return streamData.join(' '); +}); + +exports.genStreamError = functionsV2.https.onCall( + async (request, response) => { + // Note: The functions backend does not pass the error message to the + // client at this time. + throw Error("BOOM") + }); + +const weatherForecasts = { + Toronto: { conditions: 'snowy', temperature: 25 }, + London: { conditions: 'rainy', temperature: 50 }, + Dubai: { conditions: 'sunny', temperature: 75 } +}; + +/** + * Generates weather forecasts asynchronously for the given locations. + * @async + * @generator + * @param {Array<{name: string}>} locations - An array of location objects. + */ +async function* generateForecast(locations) { + for (const location of locations) { + yield { 'location': location, ...weatherForecasts[location.name] }; + await sleep(100); + } +}; + +exports.genStreamWeather = functionsV2.https.onCall( + async (request, response) => { + const locations = request.data && request.data.data? + request.data.data: []; + const forecasts = []; + if (request.acceptsStreaming) { + for await (const chunk of generateForecast(locations)) { + forecasts.push(chunk); + response.sendChunk(chunk); + } + } + return {forecasts}; + }); + +exports.genStreamEmpty = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Send no chunks + } + // Implicitly return null. + } +); + +exports.genStreamResultOnly = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Do not send any chunks. + } + return "Only a result"; + } +); + +exports.genStreamLargeData = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + const largeString = 'A'.repeat(10000); + const chunkSize = 1024; + for (let i = 0; i < largeString.length; i += chunkSize) { + const chunk = largeString.substring(i, i + chunkSize); + response.sendChunk(chunk); + await sleep(100); + } + } + return "Stream Completed"; + } +); diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt new file mode 100644 index 00000000000..e0de5cc2262 --- /dev/null +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.functions + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.initialize +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.delay +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +@RunWith(AndroidJUnit4::class) +class StreamTests { + + private lateinit var functions: FirebaseFunctions + + @Before + fun setup() { + Firebase.initialize(ApplicationProvider.getApplicationContext()) + functions = Firebase.functions + } + + internal class StreamSubscriber : Subscriber { + internal val messages = mutableListOf() + internal var result: StreamResponse.Result? = null + internal var throwable: Throwable? = null + internal var isComplete = false + internal lateinit var subscription: Subscription + + override fun onSubscribe(subscription: Subscription) { + this.subscription = subscription + subscription.request(Long.MAX_VALUE) + } + + override fun onNext(streamResponse: StreamResponse) { + if (streamResponse is StreamResponse.Message) { + messages.add(streamResponse) + } else { + result = streamResponse as StreamResponse.Result + } + } + + override fun onError(t: Throwable?) { + throwable = t + } + + override fun onComplete() { + isComplete = true + } + } + + @Test + fun genStream_withPublisher_receivesMessagesAndFinalResult() = runBlocking { + val input = mapOf("data" to "Why is the sky blue") + val function = functions.getHttpsCallable("genStream") + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages.map { it.message.data.toString() }) + .containsExactly("hello", "world", "this", "is", "cool") + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("hello world this is cool") + assertThat(subscriber.throwable).isNull() + assertThat(subscriber.isComplete).isTrue() + } + + @Test + fun genStream_withFlow_receivesMessagesAndFinalResult() = runBlocking { + val input = mapOf("data" to "Why is the sky blue") + val function = functions.getHttpsCallable("genStream") + var isComplete = false + var throwable: Throwable? = null + val messages = mutableListOf() + var result: StreamResponse.Result? = null + + val flow = function.stream(input).asFlow() + try { + withTimeout(1000) { + flow.collect { response -> + if (response is StreamResponse.Message) { + messages.add(response) + } else { + result = response as StreamResponse.Result + } + } + } + isComplete = true + } catch (e: Throwable) { + throwable = e + } + + assertThat(messages.map { it.message.data.toString() }) + .containsExactly("hello", "world", "this", "is", "cool") + assertThat(result).isNotNull() + assertThat(result!!.result.data.toString()).isEqualTo("hello world this is cool") + assertThat(throwable).isNull() + assertThat(isComplete).isTrue() + } + + @Test + fun genStreamError_receivesError() = runBlocking { + val input = mapOf("data" to "test error") + val function = + functions.getHttpsCallable("genStreamError").withTimeout(2000, TimeUnit.MILLISECONDS) + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + withTimeout(2000) { + while (subscriber.throwable == null) { + delay(100) + } + } + + assertThat(subscriber.throwable).isNotNull() + assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java) + } + + @Test + fun genStreamWeather_receivesWeatherForecasts() = runBlocking { + val inputData = listOf(mapOf("name" to "Toronto"), mapOf("name" to "London")) + val input = mapOf("data" to inputData) + + val function = functions.getHttpsCallable("genStreamWeather") + val subscriber = StreamSubscriber() + + function.stream(input).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + + assertThat(subscriber.messages.map { it.message.data.toString() }) + .containsExactly( + "{temperature=25, location={name=Toronto}, conditions=snowy}", + "{temperature=50, location={name=London}, conditions=rainy}" + ) + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).contains("forecasts") + assertThat(subscriber.throwable).isNull() + assertThat(subscriber.isComplete).isTrue() + } + + @Test + fun genStreamEmpty_receivesNoMessages() = runBlocking { + val function = functions.getHttpsCallable("genStreamEmpty") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test")).subscribe(subscriber) + + withTimeout(2000) { delay(500) } + assertThat(subscriber.messages).isEmpty() + assertThat(subscriber.result).isNull() + } + + @Test + fun genStreamResultOnly_receivesOnlyResult() = runBlocking { + val function = functions.getHttpsCallable("genStreamResultOnly") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test")).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages).isEmpty() + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Only a result") + } + + @Test + fun genStreamLargeData_receivesMultipleChunks() = runBlocking { + val function = functions.getHttpsCallable("genStreamLargeData") + val subscriber = StreamSubscriber() + + function.stream(mapOf("data" to "test large data")).subscribe(subscriber) + + while (!subscriber.isComplete) { + delay(100) + } + assertThat(subscriber.messages).isNotEmpty() + assertThat(subscriber.messages.size).isEqualTo(10) + val receivedString = + subscriber.messages.joinToString(separator = "") { it.message.data.toString() } + val expectedString = "A".repeat(10000) + assertThat(receivedString.length).isEqualTo(10000) + assertThat(receivedString).isEqualTo(expectedString) + assertThat(subscriber.result).isNotNull() + assertThat(subscriber.result!!.result.data.toString()).isEqualTo("Stream Completed") + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 824670c4346..8839763c4a3 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -45,6 +45,7 @@ import okhttp3.RequestBody import okhttp3.Response import org.json.JSONException import org.json.JSONObject +import org.reactivestreams.Publisher /** FirebaseFunctions lets you call Cloud Functions for Firebase. */ public class FirebaseFunctions @@ -311,6 +312,21 @@ internal constructor( return tcs.task } + internal fun stream( + name: String, + data: Any?, + options: HttpsCallOptions + ): Publisher = stream(getURL(name), data, options) + + internal fun stream(url: URL, data: Any?, options: HttpsCallOptions): Publisher { + val task = + providerInstalled.task.continueWithTask(executor) { + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + + return PublisherStream(url, data, options, client, this.serializer, task, executor) + } + public companion object { /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ private val providerInstalled = TaskCompletionSource() diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index 88db9db4ee4..215722584ba 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -17,6 +17,7 @@ import androidx.annotation.VisibleForTesting import com.google.android.gms.tasks.Task import java.net.URL import java.util.concurrent.TimeUnit +import org.reactivestreams.Publisher /** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ public class HttpsCallableReference { @@ -61,10 +62,8 @@ public class HttpsCallableReference { * * * Any primitive type, including null, int, long, float, and boolean. * * [String] - * * [List&lt;?&gt;][java.util.List], where the contained objects are also one of these - * types. - * * [Map&lt;String, ?&gt;>][java.util.Map], where the values are also one of these - * types. + * * [List][java.util.List], where the contained objects are also one of these types. + * * [Map][java.util.Map], where the values are also one of these types. * * [org.json.JSONArray] * * [org.json.JSONObject] * * [org.json.JSONObject.NULL] @@ -125,6 +124,55 @@ public class HttpsCallableReference { } } + /** + * Streams data to the specified HTTPS endpoint. + * + * The data passed into the trigger can be any of the following types: + * + * * Any primitive type, including null, int, long, float, and boolean. + * * [String] + * * [List][java.util.List], where the contained objects are also one of these types. + * * [Map][java.util.Map], where the values are also one of these types. + * * [org.json.JSONArray] + * * [org.json.JSONObject] + * * [org.json.JSONObject.NULL] + * + * If the returned streamResponse fails, the exception will be one of the following types: + * + * * [java.io.IOException] + * - if the HTTPS request failed to connect. + * * [FirebaseFunctionsException] + * - if the request connected, but the function returned an error. + * + * The request to the Cloud Functions backend made by this method automatically includes a + * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase + * Auth, an auth token for the user will also be automatically included. + * + * Firebase Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see + * [com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * Instance ID the next time you call this method. + * + * @param data Parameters to pass to the endpoint. Defaults to `null` if not provided. + * @return [Publisher] that will emit intermediate data, and the final result, as it is generated + * by the function. + * @see org.json.JSONArray + * + * @see org.json.JSONObject + * + * @see java.io.IOException + * + * @see FirebaseFunctionsException + */ + @JvmOverloads + public fun stream(data: Any? = null): Publisher { + return if (name != null) { + functionsClient.stream(name, data, options) + } else { + functionsClient.stream(requireNotNull(url), data, options) + } + } + /** * Changes the timeout for calls from this instance of Functions. The default is 60 seconds. * diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt new file mode 100644 index 00000000000..6fc6a9d657c --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.functions + +import com.google.android.gms.tasks.Task +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.net.URL +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicLong +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.json.JSONObject +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription + +internal class PublisherStream( + private val url: URL, + private val data: Any?, + private val options: HttpsCallOptions, + private val client: OkHttpClient, + private val serializer: Serializer, + private val contextTask: Task, + private val executor: Executor +) : Publisher { + + private val subscribers = ConcurrentLinkedQueue, AtomicLong>>() + private var activeCall: Call? = null + @Volatile private var isStreamingStarted = false + @Volatile private var isCompleted = false + private val messageQueue = ConcurrentLinkedQueue() + + override fun subscribe(subscriber: Subscriber) { + synchronized(this) { + if (isCompleted) { + subscriber.onError( + FirebaseFunctionsException( + "Cannot subscribe: Streaming has already completed.", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + return + } + subscribers.add(subscriber to AtomicLong(0)) + } + + subscriber.onSubscribe( + object : Subscription { + override fun request(n: Long) { + if (n <= 0) { + subscriber.onError(IllegalArgumentException("Requested messages must be positive.")) + return + } + + synchronized(this@PublisherStream) { + if (isCompleted) return + + val subscriberEntry = subscribers.find { it.first == subscriber } + subscriberEntry?.second?.addAndGet(n) + dispatchMessages() + if (!isStreamingStarted) { + isStreamingStarted = true + startStreaming() + } + } + } + + override fun cancel() { + synchronized(this@PublisherStream) { + notifyError( + FirebaseFunctionsException( + "Stream was canceled", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + val iterator = subscribers.iterator() + while (iterator.hasNext()) { + val pair = iterator.next() + if (pair.first == subscriber) { + iterator.remove() + } + } + if (subscribers.isEmpty()) { + cancelStream() + } + } + } + } + ) + } + + private fun startStreaming() { + contextTask.addOnCompleteListener(executor) { contextTask -> + if (!contextTask.isSuccessful) { + notifyError( + FirebaseFunctionsException( + "Error retrieving context", + FirebaseFunctionsException.Code.INTERNAL, + null, + contextTask.exception + ) + ) + return@addOnCompleteListener + } + + val context = contextTask.result + val configuredClient = options.apply(client) + val requestBody = + RequestBody.create( + MediaType.parse("application/json"), + JSONObject(mapOf("data" to serializer.encode(data))).toString() + ) + val requestBuilder = + Request.Builder().url(url).post(requestBody).header("Accept", "text/event-stream") + context?.authToken?.let { requestBuilder.header("Authorization", "Bearer $it") } + context?.instanceIdToken?.let { requestBuilder.header("Firebase-Instance-ID-Token", it) } + context?.appCheckToken?.let { requestBuilder.header("X-Firebase-AppCheck", it) } + val request = requestBuilder.build() + val call = configuredClient.newCall(request) + activeCall = call + + call.enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + val code: FirebaseFunctionsException.Code = + if (e is InterruptedIOException) { + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED + } else { + FirebaseFunctionsException.Code.INTERNAL + } + notifyError(FirebaseFunctionsException(code.name, code, null, e)) + } + + override fun onResponse(call: Call, response: Response) { + validateResponse(response) + val bodyStream = response.body()?.byteStream() + if (bodyStream != null) { + processSSEStream(bodyStream) + } else { + notifyError( + FirebaseFunctionsException( + "Response body is null", + FirebaseFunctionsException.Code.INTERNAL, + null + ) + ) + } + } + } + ) + } + } + + private fun cancelStream() { + activeCall?.cancel() + notifyError( + FirebaseFunctionsException( + "Stream was canceled", + FirebaseFunctionsException.Code.CANCELLED, + null + ) + ) + } + + private fun processSSEStream(inputStream: InputStream) { + BufferedReader(InputStreamReader(inputStream)).use { reader -> + try { + val eventBuffer = StringBuilder() + reader.lineSequence().forEach { line -> + if (line.isBlank()) { + processEvent(eventBuffer.toString()) + eventBuffer.clear() + } else { + val dataChunk = + when { + line.startsWith("data:") -> line.removePrefix("data:") + line.startsWith("result:") -> line.removePrefix("result:") + else -> return@forEach + } + eventBuffer.append(dataChunk.trim()).append("\n") + } + } + } catch (e: Exception) { + notifyError( + FirebaseFunctionsException( + e.message ?: "Error reading stream", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + ) + } + } + } + + private fun processEvent(dataChunk: String) { + try { + val json = JSONObject(dataChunk) + when { + json.has("message") -> { + serializer.decode(json.opt("message"))?.let { + messageQueue.add(StreamResponse.Message(message = HttpsCallableResult(it))) + } + dispatchMessages() + } + json.has("error") -> { + serializer.decode(json.opt("error"))?.let { + notifyError( + FirebaseFunctionsException( + it.toString(), + FirebaseFunctionsException.Code.INTERNAL, + it + ) + ) + } + } + json.has("result") -> { + serializer.decode(json.opt("result"))?.let { + messageQueue.add(StreamResponse.Result(result = HttpsCallableResult(it))) + dispatchMessages() + notifyComplete() + } + } + } + } catch (e: Throwable) { + notifyError( + FirebaseFunctionsException( + "Invalid JSON: $dataChunk", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + ) + } + } + + private fun dispatchMessages() { + synchronized(this) { + val iterator = subscribers.iterator() + while (iterator.hasNext()) { + val (subscriber, requestedCount) = iterator.next() + while (requestedCount.get() > 0 && messageQueue.isNotEmpty()) { + subscriber.onNext(messageQueue.poll()) + requestedCount.decrementAndGet() + } + } + } + } + + private fun notifyError(e: Throwable) { + if (!isCompleted) { + isCompleted = true + subscribers.forEach { (subscriber, _) -> + try { + subscriber.onError(e) + } catch (ignored: Exception) {} + } + subscribers.clear() + messageQueue.clear() + } + } + + private fun notifyComplete() { + if (!isCompleted) { + isCompleted = true + subscribers.forEach { (subscriber, _) -> subscriber.onComplete() } + subscribers.clear() + messageQueue.clear() + } + } + + private fun validateResponse(response: Response) { + if (response.isSuccessful) return + + val htmlContentType = "text/html; charset=utf-8" + val errorMessage: String + if (response.code() == 404 && response.header("Content-Type") == htmlContentType) { + errorMessage = """URL not found. Raw response: ${response.body()?.string()}""".trimMargin() + throw FirebaseFunctionsException( + errorMessage, + FirebaseFunctionsException.Code.fromHttpStatus(response.code()), + null + ) + } + + val text = response.body()?.string() ?: "" + val error: Any? + try { + val json = JSONObject(text) + error = serializer.decode(json.opt("error")) + } catch (e: Throwable) { + throw FirebaseFunctionsException( + "${e.message} Unexpected Response:\n$text ", + FirebaseFunctionsException.Code.INTERNAL, + e + ) + } + throw FirebaseFunctionsException( + error.toString(), + FirebaseFunctionsException.Code.INTERNAL, + error + ) + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt new file mode 100644 index 00000000000..123f804614d --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/StreamResponse.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.functions + +/** + * Represents a response from a Server-Sent Event (SSE) stream. + * + * The SSE stream consists of two types of responses: + * - [Message]: Represents an intermediate event pushed from the server. + * - [Result]: Represents the final response that signifies the stream has ended. + */ +public abstract class StreamResponse private constructor() { + + /** + * An event message received during the stream. + * + * Messages are intermediate data chunks sent by the server while processing a request. + * + * Example SSE format: + * ```json + * data: { "message": { "chunk": "foo" } } + * ``` + * + * @property message the intermediate data received from the server. + */ + public class Message(public val message: HttpsCallableResult) : StreamResponse() + + /** + * The final result of the computation, marking the end of the stream. + * + * Unlike [Message], which represents intermediate data chunks, [Result] contains the complete + * computation output. If clients only care about the final result, they can process this type + * alone and ignore intermediate messages. + * + * Example SSE format: + * ```json + * data: { "result": { "text": "foo bar" } } + * ``` + * + * @property result the final computed result received from the server. + */ + public class Result(public val result: HttpsCallableResult) : StreamResponse() +} From dfd0d7c3710fcaa63184b72d55693b7b8a050ce7 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 10 Mar 2025 17:32:51 -0400 Subject: [PATCH 031/162] [Functions] Send the placeholder appcheck token in case of error (#6750) tracking b/399116207 --------- Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com> --- firebase-functions/CHANGELOG.md | 1 + .../functions/FirebaseContextProviderTest.java | 10 ++++++---- .../firebase/functions/FirebaseContextProvider.kt | 4 +--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 26bb8de7306..863de38db50 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error. # 21.1.1 * [fixed] Resolve Kotlin migration visibility issues diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java index 1126ae55fbb..384230867d9 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java @@ -117,7 +117,7 @@ public void getContext_whenOnlyAuthIsAvailableAndNotSignedIn_shouldContainOnlyIi } @Test - public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid() + public void getContext_whenOnlyAppCheckIsAvailableAndHasError() throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( @@ -129,11 +129,12 @@ public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyI HttpsCallableContext context = Tasks.await(contextProvider.getContext(false)); assertThat(context.getAuthToken()).isNull(); assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN); - assertThat(context.getAppCheckToken()).isNull(); + // AppCheck token needs to be send in all circumstances. + assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_TOKEN); } @Test - public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyIid() + public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError() throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( @@ -145,7 +146,8 @@ public void getContext_facLimitedUse_whenOnlyAppCheckIsAvailableAndHasError_shou HttpsCallableContext context = Tasks.await(contextProvider.getContext(true)); assertThat(context.getAuthToken()).isNull(); assertThat(context.getInstanceIdToken()).isEqualTo(IID_TOKEN); - assertThat(context.getAppCheckToken()).isNull(); + // AppCheck token needs to be sent in all circumstances. + assertThat(context.getAppCheckToken()).isEqualTo(APP_CHECK_LIMITED_USE_TOKEN); } @Test diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt index 7ab1f74bf5d..96f18eb2c05 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt @@ -88,11 +88,9 @@ constructor( if (getLimitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false) return tokenTask.onSuccessTask(executor) { result: AppCheckTokenResult -> if (result.error != null) { - // If there was an error getting the App Check token, do NOT send the placeholder - // token. Only valid App Check tokens should be sent to the functions backend. Log.w(TAG, "Error getting App Check token. Error: " + result.error) - return@onSuccessTask Tasks.forResult(null) } + // Send valid token (success) or placeholder (failure). Tasks.forResult(result.token) } } From 68f52edce65e36d64641ee8a0750a29068935614 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 11 Mar 2025 07:16:08 -0600 Subject: [PATCH 032/162] Read version control info from Android resource (#6754) --- firebase-crashlytics/CHANGELOG.md | 1 + .../internal/common/CommonUtils.java | 11 +++++++ .../common/CrashlyticsController.java | 31 ++++++++++++------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index a6e5e087b60..c0e6ddce86b 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Internal changes to read version control info more efficiently [6754] * [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720] # 19.4.1 diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java index b29863f66c5..a116cf55542 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java @@ -69,6 +69,8 @@ public class CommonUtils { "com.google.firebase.crashlytics.build_ids_arch"; static final String BUILD_IDS_BUILD_ID_RESOURCE_NAME = "com.google.firebase.crashlytics.build_ids_build_id"; + static final String VERSION_CONTROL_INFO_RESOURCE_NAME = + "com.google.firebase.crashlytics.version_control_info"; // TODO: Maybe move this method into a more appropriate class. public static SharedPreferences getSharedPrefs(Context context) { @@ -525,6 +527,15 @@ public static List getBuildIdInfo(Context context) { return buildIdInfoList; } + @Nullable + public static String getVersionControlInfo(Context context) { + int id = getResourcesIdentifier(context, VERSION_CONTROL_INFO_RESOURCE_NAME, "string"); + if (id == 0) { + return null; + } + return context.getResources().getString(id); + } + public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index b55a26678d4..da28e8708db 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -48,6 +48,7 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -77,6 +78,8 @@ class CrashlyticsController { private static final String VERSION_CONTROL_INFO_FILE = "version-control-info.textproto"; private static final String META_INF_FOLDER = "META-INF/"; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final Context context; private final DataCollectionArbiter dataCollectionArbiter; private final CrashlyticsFileMarker crashMarker; @@ -628,13 +631,23 @@ void saveVersionControlInfo() { } String getVersionControlInfo() throws IOException { - InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE); - if (is == null) { - return null; + // Attempt to read from an Android string resource + String versionControlInfo = CommonUtils.getVersionControlInfo(context); + if (versionControlInfo != null) { + Logger.getLogger().d("Read version control info from string resource"); + return Base64.encodeToString(versionControlInfo.getBytes(UTF_8), 0); + } + + // Fallback to reading the file + try (InputStream is = getResourceAsStream(META_INF_FOLDER + VERSION_CONTROL_INFO_FILE)) { + if (is != null) { + Logger.getLogger().d("Read version control info from file"); + return Base64.encodeToString(readResource(is), 0); + } } - Logger.getLogger().d("Read version control info"); - return Base64.encodeToString(readResource(is), 0); + Logger.getLogger().i("No version control information found"); + return null; } private InputStream getResourceAsStream(String resource) { @@ -644,13 +657,7 @@ private InputStream getResourceAsStream(String resource) { return null; } - InputStream is = classLoader.getResourceAsStream(resource); - if (is == null) { - Logger.getLogger().i("No version control information found"); - return null; - } - - return is; + return classLoader.getResourceAsStream(resource); } private static byte[] readResource(InputStream is) throws IOException { From 934050322f5b3ecb07d759174f784be62baac9b2 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 11 Mar 2025 07:19:43 -0600 Subject: [PATCH 033/162] Use Dagger for dependency injection in Sessions (#6745) --- firebase-sessions/CHANGELOG.md | 1 + .../firebase-sessions.gradle.kts | 10 +- .../firebase/sessions/EventGDTLogger.kt | 14 +-- .../firebase/sessions/FirebaseSessions.kt | 10 +- .../sessions/FirebaseSessionsComponent.kt | 86 +++++++++++++++++ .../sessions/FirebaseSessionsRegistrar.kt | 94 +++++-------------- .../firebase/sessions/SessionDatastore.kt | 27 +++--- .../sessions/SessionFirelogPublisher.kt | 12 ++- .../firebase/sessions/SessionGenerator.kt | 8 +- .../sessions/SessionLifecycleService.kt | 1 - .../sessions/SessionLifecycleServiceBinder.kt | 10 +- .../sessions/settings/SessionsSettings.kt | 13 ++- .../testing/FirebaseSessionsFakeComponent.kt | 46 +++++++++ .../testing/FirebaseSessionsFakeRegistrar.kt | 30 +++--- 14 files changed, 238 insertions(+), 124 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 2473b64a1cf..d5293913dc9 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* [changed] Use Dagger for dependency injection * [changed] Updated datastore dependency to `1.1.3` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts index 0a09740bd77..b136a281660 100644 --- a/firebase-sessions/firebase-sessions.gradle.kts +++ b/firebase-sessions/firebase-sessions.gradle.kts @@ -18,6 +18,7 @@ plugins { id("firebase-library") + id("firebase-vendor") id("kotlin-android") id("kotlin-kapt") } @@ -67,12 +68,18 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-common") exclude(group = "com.google.firebase", module = "firebase-components") } - implementation("com.google.android.datatransport:transport-api:3.2.0") + api("com.google.firebase:firebase-annotations:16.2.0") api("com.google.firebase:firebase-encoders:17.0.0") api("com.google.firebase:firebase-encoders-json:18.0.1") + + implementation("com.google.android.datatransport:transport-api:3.2.0") + implementation(libs.javax.inject) implementation(libs.androidx.annotation) implementation(libs.androidx.datastore.preferences) + + vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") } + compileOnly(libs.errorprone.annotations) runtimeOnly("com.google.firebase:firebase-installations:18.0.0") { @@ -85,6 +92,7 @@ dependencies { } kapt(project(":encoders:firebase-encoders-processor")) + kapt(libs.dagger.compiler) testImplementation(project(":integ-testing")) { exclude(group = "com.google.firebase", module = "firebase-common") diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt index a11b20a7d5c..496cc70d36d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/EventGDTLogger.kt @@ -21,6 +21,8 @@ import com.google.android.datatransport.Encoding import com.google.android.datatransport.Event import com.google.android.datatransport.TransportFactory import com.google.firebase.inject.Provider +import javax.inject.Inject +import javax.inject.Singleton /** * The [EventGDTLoggerInterface] is for testing purposes so that we can mock EventGDTLogger in other @@ -38,19 +40,17 @@ internal fun interface EventGDTLoggerInterface { * * @hide */ -internal class EventGDTLogger(private val transportFactoryProvider: Provider) : +@Singleton +internal class EventGDTLogger +@Inject +constructor(private val transportFactoryProvider: Provider) : EventGDTLoggerInterface { // Logs a [SessionEvent] to FireLog override fun log(sessionEvent: SessionEvent) { transportFactoryProvider .get() - .getTransport( - AQS_LOG_SOURCE, - SessionEvent::class.java, - Encoding.of("json"), - this::encode, - ) + .getTransport(AQS_LOG_SOURCE, SessionEvent::class.java, Encoding.of("json"), this::encode) .send(Event.ofData(sessionEvent)) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 0dec3b98150..18b9961724b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -20,18 +20,24 @@ import android.app.Application import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Responsible for initializing AQS */ -internal class FirebaseSessions( +@Singleton +internal class FirebaseSessions +@Inject +constructor( private val firebaseApp: FirebaseApp, private val settings: SessionsSettings, - backgroundDispatcher: CoroutineContext, + @Background backgroundDispatcher: CoroutineContext, lifecycleServiceBinder: SessionLifecycleServiceBinder, ) { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt new file mode 100644 index 00000000000..aa60f3f41df --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions + +import android.content.Context +import com.google.android.datatransport.TransportFactory +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.inject.Provider +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.settings.SessionsSettings +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext + +/** Dagger component to provide [FirebaseSessions] and its dependencies. */ +@Singleton +@Component(modules = [FirebaseSessionsComponent.MainModule::class]) +internal interface FirebaseSessionsComponent { + val firebaseSessions: FirebaseSessions + + val sessionDatastore: SessionDatastore + val sessionFirelogPublisher: SessionFirelogPublisher + val sessionGenerator: SessionGenerator + val sessionsSettings: SessionsSettings + + @Component.Builder + interface Builder { + @BindsInstance fun appContext(appContext: Context): Builder + + @BindsInstance + fun backgroundDispatcher(@Background backgroundDispatcher: CoroutineContext): Builder + + @BindsInstance fun blockingDispatcher(@Blocking blockingDispatcher: CoroutineContext): Builder + + @BindsInstance fun firebaseApp(firebaseApp: FirebaseApp): Builder + + @BindsInstance + fun firebaseInstallationsApi(firebaseInstallationsApi: FirebaseInstallationsApi): Builder + + @BindsInstance + fun transportFactoryProvider(transportFactoryProvider: Provider): Builder + + fun build(): FirebaseSessionsComponent + } + + @Module + interface MainModule { + @Binds @Singleton fun eventGDTLoggerInterface(impl: EventGDTLogger): EventGDTLoggerInterface + + @Binds @Singleton fun sessionDatastore(impl: SessionDatastoreImpl): SessionDatastore + + @Binds + @Singleton + fun sessionFirelogPublisher(impl: SessionFirelogPublisherImpl): SessionFirelogPublisher + + @Binds + @Singleton + fun sessionLifecycleServiceBinder( + impl: SessionLifecycleServiceBinderImpl + ): SessionLifecycleServiceBinder + + companion object { + @Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock) + } + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index caad2de6ff8..1043ad74800 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -16,6 +16,7 @@ package com.google.firebase.sessions +import android.content.Context import androidx.annotation.Keep import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp @@ -28,7 +29,6 @@ import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent -import com.google.firebase.sessions.settings.SessionsSettings import kotlinx.coroutines.CoroutineDispatcher /** @@ -42,87 +42,41 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { listOf( Component.builder(FirebaseSessions::class.java) .name(LIBRARY_NAME) - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(sessionsSettings)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(sessionLifecycleServiceBinder)) - .factory { container -> - FirebaseSessions( - container[firebaseApp], - container[sessionsSettings], - container[backgroundDispatcher], - container[sessionLifecycleServiceBinder], - ) - } + .add(Dependency.required(firebaseSessionsComponent)) + .factory { container -> container[firebaseSessionsComponent].firebaseSessions } .eagerInDefaultApp() .build(), - Component.builder(SessionGenerator::class.java) - .name("session-generator") - .factory { SessionGenerator(timeProvider = WallClock) } - .build(), - Component.builder(SessionFirelogPublisher::class.java) - .name("session-publisher") - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(firebaseInstallationsApi)) - .add(Dependency.required(sessionsSettings)) - .add(Dependency.requiredProvider(transportFactory)) + Component.builder(FirebaseSessionsComponent::class.java) + .name("fire-sessions-component") + .add(Dependency.required(appContext)) .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - SessionFirelogPublisherImpl( - container[firebaseApp], - container[firebaseInstallationsApi], - container[sessionsSettings], - EventGDTLogger(container.getProvider(transportFactory)), - container[backgroundDispatcher], - ) - } - .build(), - Component.builder(SessionsSettings::class.java) - .name("sessions-settings") - .add(Dependency.required(firebaseApp)) .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(firebaseInstallationsApi)) - .factory { container -> - SessionsSettings( - container[firebaseApp], - container[blockingDispatcher], - container[backgroundDispatcher], - container[firebaseInstallationsApi], - ) - } - .build(), - Component.builder(SessionDatastore::class.java) - .name("sessions-datastore") .add(Dependency.required(firebaseApp)) - .add(Dependency.required(backgroundDispatcher)) + .add(Dependency.required(firebaseInstallationsApi)) + .add(Dependency.requiredProvider(transportFactory)) .factory { container -> - SessionDatastoreImpl( - container[firebaseApp].applicationContext, - container[backgroundDispatcher], - ) + DaggerFirebaseSessionsComponent.builder() + .appContext(container[appContext]) + .backgroundDispatcher(container[backgroundDispatcher]) + .blockingDispatcher(container[blockingDispatcher]) + .firebaseApp(container[firebaseApp]) + .firebaseInstallationsApi(container[firebaseInstallationsApi]) + .transportFactoryProvider(container.getProvider(transportFactory)) + .build() } .build(), - Component.builder(SessionLifecycleServiceBinder::class.java) - .name("sessions-service-binder") - .add(Dependency.required(firebaseApp)) - .factory { container -> SessionLifecycleServiceBinderImpl(container[firebaseApp]) } - .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) private companion object { - private const val LIBRARY_NAME = "fire-sessions" + const val LIBRARY_NAME = "fire-sessions" - private val firebaseApp = unqualified(FirebaseApp::class.java) - private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) - private val backgroundDispatcher = - qualified(Background::class.java, CoroutineDispatcher::class.java) - private val blockingDispatcher = - qualified(Blocking::class.java, CoroutineDispatcher::class.java) - private val transportFactory = unqualified(TransportFactory::class.java) - private val sessionsSettings = unqualified(SessionsSettings::class.java) - private val sessionLifecycleServiceBinder = - unqualified(SessionLifecycleServiceBinder::class.java) + val appContext = unqualified(Context::class.java) + val firebaseApp = unqualified(FirebaseApp::class.java) + val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) + val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) + val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) + val transportFactory = unqualified(TransportFactory::class.java) + val firebaseSessionsComponent = unqualified(FirebaseSessionsComponent::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index 736761617fd..a2d46a48891 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -26,10 +26,13 @@ import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import java.io.IOException import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -53,13 +56,16 @@ internal interface SessionDatastore { companion object { val instance: SessionDatastore - get() = Firebase.app[SessionDatastore::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionDatastore } } -internal class SessionDatastoreImpl( - private val context: Context, - private val backgroundDispatcher: CoroutineContext, +@Singleton +internal class SessionDatastoreImpl +@Inject +constructor( + private val appContext: Context, + @Background private val backgroundDispatcher: CoroutineContext, ) : SessionDatastore { /** Most recent session from datastore is updated asynchronously whenever it changes */ @@ -70,7 +76,7 @@ internal class SessionDatastoreImpl( } private val firebaseSessionDataFlow: Flow = - context.dataStore.data + appContext.dataStore.data .catch { exception -> Log.e(TAG, "Error reading stored session data.", exception) emit(emptyPreferences()) @@ -86,14 +92,11 @@ internal class SessionDatastoreImpl( override fun updateSessionId(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { try { - context.dataStore.edit { preferences -> + appContext.dataStore.edit { preferences -> preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId } } catch (e: IOException) { - Log.w( - TAG, - "Failed to update session Id: $e", - ) + Log.w(TAG, "Failed to update session Id: $e") } } } @@ -101,9 +104,7 @@ internal class SessionDatastoreImpl( override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData = - FirebaseSessionsData( - preferences[FirebaseSessionDataKeys.SESSION_ID], - ) + FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID]) private companion object { private const val TAG = "FirebaseSessionsRepo" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index d63d49e3fe5..6e4b6153f8d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -19,10 +19,13 @@ package com.google.firebase.sessions import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.settings.SessionsSettings +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -35,7 +38,7 @@ internal fun interface SessionFirelogPublisher { companion object { val instance: SessionFirelogPublisher - get() = Firebase.app[SessionFirelogPublisher::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionFirelogPublisher } } @@ -44,12 +47,15 @@ internal fun interface SessionFirelogPublisher { * * @hide */ -internal class SessionFirelogPublisherImpl( +@Singleton +internal class SessionFirelogPublisherImpl +@Inject +constructor( private val firebaseApp: FirebaseApp, private val firebaseInstallations: FirebaseInstallationsApi, private val sessionSettings: SessionsSettings, private val eventGDTLogger: EventGDTLoggerInterface, - private val backgroundDispatcher: CoroutineContext, + @Background private val backgroundDispatcher: CoroutineContext, ) : SessionFirelogPublisher { /** diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 3b4c3124c98..41aeb442cfb 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -20,6 +20,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue import com.google.firebase.Firebase import com.google.firebase.app import java.util.UUID +import javax.inject.Singleton /** * [SessionDetails] is a data class responsible for storing information about the current Session. @@ -35,9 +36,10 @@ internal data class SessionDetails( * The [SessionGenerator] is responsible for generating the Session ID, and keeping the * [SessionDetails] up to date with the latest values. */ +@Singleton internal class SessionGenerator( private val timeProvider: TimeProvider, - private val uuidGenerator: () -> UUID = UUID::randomUUID + private val uuidGenerator: () -> UUID = UUID::randomUUID, ) { private val firstSessionId = generateSessionId() private var sessionIndex = -1 @@ -59,7 +61,7 @@ internal class SessionGenerator( sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(), firstSessionId, sessionIndex, - sessionStartTimestampUs = timeProvider.currentTimeUs() + sessionStartTimestampUs = timeProvider.currentTimeUs(), ) return currentSession } @@ -68,6 +70,6 @@ internal class SessionGenerator( internal companion object { val instance: SessionGenerator - get() = Firebase.app[SessionGenerator::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionGenerator } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index bde6d138fbe..6807d8bec69 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -128,7 +128,6 @@ internal class SessionLifecycleService : Service() { /** Generates a new session id and sends it everywhere it's needed */ private fun newSession() { try { - // TODO(mrober): Consider migrating to Dagger, or update [FirebaseSessionsRegistrar]. SessionGenerator.instance.generateNewSession() Log.d(TAG, "Generated new session.") broadcastSession() diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt index 97a7d6b73ae..094a76ee51c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt @@ -21,7 +21,8 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Messenger import android.util.Log -import com.google.firebase.FirebaseApp +import javax.inject.Inject +import javax.inject.Singleton /** Interface for binding with the [SessionLifecycleService]. */ internal fun interface SessionLifecycleServiceBinder { @@ -32,11 +33,12 @@ internal fun interface SessionLifecycleServiceBinder { fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) } -internal class SessionLifecycleServiceBinderImpl(private val firebaseApp: FirebaseApp) : - SessionLifecycleServiceBinder { +@Singleton +internal class SessionLifecycleServiceBinderImpl +@Inject +constructor(private val appContext: Context) : SessionLifecycleServiceBinder { override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { - val appContext: Context = firebaseApp.applicationContext.applicationContext Intent(appContext, SessionLifecycleService::class.java).also { intent -> Log.d(TAG, "Binding service to application.") // This is necessary for the onBind() to be called by each process diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index fd2ee5dbddd..41b73f14a4e 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -25,17 +25,23 @@ import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo +import com.google.firebase.sessions.FirebaseSessionsComponent import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import com.google.firebase.sessions.SessionDataStoreConfigs import com.google.firebase.sessions.SessionEvents +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes /** [SessionsSettings] manages all the configs that are relevant to the sessions library. */ +@Singleton internal class SessionsSettings( private val localOverrideSettings: SettingsProvider, private val remoteSettings: SettingsProvider, @@ -62,10 +68,11 @@ internal class SessionsSettings( ), ) + @Inject constructor( firebaseApp: FirebaseApp, - blockingDispatcher: CoroutineContext, - backgroundDispatcher: CoroutineContext, + @Blocking blockingDispatcher: CoroutineContext, + @Background backgroundDispatcher: CoroutineContext, firebaseInstallationsApi: FirebaseInstallationsApi, ) : this( firebaseApp.applicationContext, @@ -143,7 +150,7 @@ internal class SessionsSettings( private const val TAG = "SessionsSettings" val instance: SessionsSettings - get() = Firebase.app[SessionsSettings::class.java] + get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionsSettings private val Context.dataStore: DataStore by preferencesDataStore( diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt new file mode 100644 index 00000000000..eda16d8f0b4 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions.testing + +import com.google.firebase.Firebase +import com.google.firebase.app +import com.google.firebase.sessions.FirebaseSessions +import com.google.firebase.sessions.FirebaseSessionsComponent +import com.google.firebase.sessions.SessionDatastore +import com.google.firebase.sessions.SessionFirelogPublisher +import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.settings.SessionsSettings + +/** Bridge between FirebaseSessionsComponent and FirebaseSessionsFakeRegistrar. */ +internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent { + // TODO(mrober): Move tests to use Dagger for DI. + + override val firebaseSessions: FirebaseSessions + get() = Firebase.app[FirebaseSessions::class.java] + + override val sessionDatastore: SessionDatastore + get() = Firebase.app[SessionDatastore::class.java] + + override val sessionFirelogPublisher: SessionFirelogPublisher + get() = Firebase.app[SessionFirelogPublisher::class.java] + + override val sessionGenerator: SessionGenerator + get() = Firebase.app[SessionGenerator::class.java] + + override val sessionsSettings: SessionsSettings + get() = Firebase.app[SessionsSettings::class.java] +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index 9755a5e12d0..58855f622f3 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -17,7 +17,6 @@ package com.google.firebase.sessions.testing import androidx.annotation.Keep -import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking @@ -26,10 +25,10 @@ import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified -import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent import com.google.firebase.sessions.BuildConfig import com.google.firebase.sessions.FirebaseSessions +import com.google.firebase.sessions.FirebaseSessionsComponent import com.google.firebase.sessions.SessionDatastore import com.google.firebase.sessions.SessionFirelogPublisher import com.google.firebase.sessions.SessionGenerator @@ -75,6 +74,10 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { ) } .build(), + Component.builder(FirebaseSessionsComponent::class.java) + .name("fake-fire-sessions-component") + .factory { FirebaseSessionsFakeComponent() } + .build(), Component.builder(FakeSessionDatastore::class.java) .name("fake-sessions-datastore") .factory { FakeSessionDatastore() } @@ -97,21 +100,14 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { ) private companion object { - private const val LIBRARY_NAME = "fire-sessions" - - private val firebaseApp = unqualified(FirebaseApp::class.java) - private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) - private val backgroundDispatcher = - qualified(Background::class.java, CoroutineDispatcher::class.java) - private val blockingDispatcher = - qualified(Blocking::class.java, CoroutineDispatcher::class.java) - private val transportFactory = unqualified(TransportFactory::class.java) - private val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) - private val fakeDatastore = unqualified(FakeSessionDatastore::class.java) - private val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) - private val sessionGenerator = unqualified(SessionGenerator::class.java) - private val sessionsSettings = unqualified(SessionsSettings::class.java) + const val LIBRARY_NAME = "fire-sessions" - private val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val firebaseApp = unqualified(FirebaseApp::class.java) + val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) + val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) + val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) + val fakeDatastore = unqualified(FakeSessionDatastore::class.java) + val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) + val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") } } From c9287ee0f33a5fee1682030b28ed4523df750064 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 11 Mar 2025 09:14:56 -0600 Subject: [PATCH 034/162] Add warning for known issue b/328687152 (#6755) --- firebase-sessions/CHANGELOG.md | 1 + .../sessions/FirebaseSessionsRegistrar.kt | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index d5293913dc9..7285ff94e27 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* [changed] Add warning for known issue b/328687152 * [changed] Use Dagger for dependency injection * [changed] Updated datastore dependency to `1.1.3` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 1043ad74800..5cb8de7a182 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -17,7 +17,9 @@ package com.google.firebase.sessions import android.content.Context +import android.util.Log import androidx.annotation.Keep +import androidx.datastore.preferences.preferencesDataStore import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background @@ -69,6 +71,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { ) private companion object { + const val TAG = "FirebaseSessions" const val LIBRARY_NAME = "fire-sessions" val appContext = unqualified(Context::class.java) @@ -78,5 +81,29 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) val transportFactory = unqualified(TransportFactory::class.java) val firebaseSessionsComponent = unqualified(FirebaseSessionsComponent::class.java) + + init { + try { + ::preferencesDataStore.javaClass + } catch (ex: NoClassDefFoundError) { + Log.w( + TAG, + """ + Your app is experiencing a known issue in the Android Gradle plugin, see https://issuetracker.google.com/328687152 + + It affects Java-only apps using AGP version 8.3.2 and under. To avoid the issue, either: + + 1. Upgrade Android Gradle plugin to 8.4.0+ + Follow the guide at https://developer.android.com/build/agp-upgrade-assistant + + 2. Or, add the Kotlin plugin to your app + Follow the guide at https://developer.android.com/kotlin/add-kotlin + + 3. Or, do the technical workaround described in https://issuetracker.google.com/issues/328687152#comment3 + """ + .trimIndent(), + ) + } + } } } From d0fd4caafc17b368252bd6e937587929f0b18097 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Tue, 11 Mar 2025 11:53:19 -0700 Subject: [PATCH 035/162] m160 mergeback (#6757) Auto-generated PR for cleaning up release m160 NO_RELEASE_CHANGE --------- Co-authored-by: VinayGuthal Co-authored-by: VinayGuthal --- firebase-crashlytics-ndk/CHANGELOG.md | 3 +++ firebase-crashlytics-ndk/gradle.properties | 4 ++-- firebase-crashlytics/CHANGELOG.md | 1 + firebase-crashlytics/gradle.properties | 4 ++-- firebase-functions/CHANGELOG.md | 2 ++ firebase-functions/gradle.properties | 4 ++-- firebase-sessions/CHANGELOG.md | 8 +++++++- firebase-sessions/gradle.properties | 4 ++-- firebase-vertexai/CHANGELOG.md | 3 +++ firebase-vertexai/gradle.properties | 4 ++-- 10 files changed, 26 insertions(+), 11 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index ab8dd8dfdf3..0cf17d1d025 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased + + +# 19.4.1 * [changed] Updated `firebase-crashlytics` dependency to v19.4.1 # 19.3.0 diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties index 5ab96e1d760..a7ea562fe0f 100644 --- a/firebase-crashlytics-ndk/gradle.properties +++ b/firebase-crashlytics-ndk/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.1 -latestReleasedVersion=19.4.0 +version=19.4.2 +latestReleasedVersion=19.4.1 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index c0e6ddce86b..723e4a2e7d1 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -2,6 +2,7 @@ * [changed] Internal changes to read version control info more efficiently [6754] * [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720] + # 19.4.1 * [changed] Updated `firebase-sessions` dependency to v2.0.9 diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties index 5ab96e1d760..a7ea562fe0f 100644 --- a/firebase-crashlytics/gradle.properties +++ b/firebase-crashlytics/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.1 -latestReleasedVersion=19.4.0 +version=19.4.2 +latestReleasedVersion=19.4.1 diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 863de38db50..785f0f9966a 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error. + # 21.1.1 * [fixed] Resolve Kotlin migration visibility issues ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522)) @@ -225,3 +226,4 @@ updates. optional region to override the default "us-central1". * [feature] New `useFunctionsEmulator` method allows testing against a local instance of the [Cloud Functions Emulator](https://firebase.google.com/docs/functions/local-emulator). + diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index ff0fa6afed0..6fe83923849 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=21.1.1 -latestReleasedVersion=21.1.0 +version=21.1.2 +latestReleasedVersion=21.1.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 7285ff94e27..eb5a6b85596 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,13 +1,19 @@ # Unreleased - * [changed] Add warning for known issue b/328687152 * [changed] Use Dagger for dependency injection * [changed] Updated datastore dependency to `1.1.3` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). + # 2.0.9 * [fixed] Make AQS resilient to background init in multi-process apps. + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-sessions` library. The Kotlin extensions library has no additional +updates. + # 2.0.7 * [fixed] Removed extraneous logs that risk leaking internal identifiers. diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties index c9bd869d4cd..6a74cb4445b 100644 --- a/firebase-sessions/gradle.properties +++ b/firebase-sessions/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=2.0.9 -latestReleasedVersion=2.0.8 +version=2.0.10 +latestReleasedVersion=2.0.9 diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index c0bfdfb214e..e28c285822e 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased + + +# 16.2.0 * [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`. * [changed] Added support for modality-based token count. (#6658) * [feature] Added support for generating images with Imagen models. diff --git a/firebase-vertexai/gradle.properties b/firebase-vertexai/gradle.properties index b686fdcb9db..546c015493e 100644 --- a/firebase-vertexai/gradle.properties +++ b/firebase-vertexai/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.2.0 -latestReleasedVersion=16.1.0 +version=16.2.1 +latestReleasedVersion=16.2.0 From 9f6cacbf466afc28b7359137c62c1453cc80762c Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 11 Mar 2025 14:59:20 -0400 Subject: [PATCH 036/162] [Vertex AI] Remove `golden-files` directory (#6740) Remove the `firebase-vertexai/src/test/resources/golden-files` directory. This was carried over from the [`generative-ai-android`](https://github.com/google-gemini/generative-ai-android) repository. We are now using https://github.com/FirebaseExtended/vertexai-sdk-test-data/tree/main/mock-responses instead. #no-changelog --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com> Co-authored-by: Matthew Robertson Co-authored-by: Rodrigo Lazo Paz --- .../vertexai/common/StreamingSnapshotTests.kt | 16 +---- .../vertexai/common/UnarySnapshotTests.kt | 15 +--- .../firebase/vertexai/common/util/tests.kt | 7 +- .../streaming/failure-api-key.txt | 21 ------ .../streaming/failure-empty-content.txt | 1 - .../failure-finish-reason-safety.txt | 2 - .../streaming/failure-http-error.txt | 13 ---- .../streaming/failure-image-rejected.txt | 7 -- .../failure-prompt-blocked-safety.txt | 2 - .../failure-recitation-no-content.txt | 6 -- .../streaming/failure-unknown-model.txt | 13 ---- .../streaming/success-basic-reply-long.txt | 12 ---- .../streaming/success-basic-reply-short.txt | 2 - .../streaming/success-citations-altname.txt | 12 ---- .../streaming/success-citations.txt | 12 ---- .../streaming/success-quotes-escaped.txt | 7 -- .../streaming/success-unknown-enum.txt | 11 --- .../golden-files/unary/failure-api-key.json | 21 ------ .../unary/failure-empty-content.json | 28 -------- .../unary/failure-finish-reason-safety.json | 54 -------------- .../unary/failure-http-error.json | 13 ---- .../unary/failure-image-rejected.json | 13 ---- .../unary/failure-invalid-response.json | 14 ---- .../unary/failure-malformed-content.json | 30 -------- .../unary/failure-prompt-blocked-safety.json | 23 ------ .../unary/failure-quota-exceeded.json | 31 -------- .../unary/failure-service-disabled.json | 27 ------- .../unary/failure-unknown-model.json | 13 ---- .../failure-unsupported-user-location.json | 13 ---- .../unary/success-basic-reply-long.json | 54 -------------- .../unary/success-basic-reply-short.json | 54 -------------- .../unary/success-citations-altname.json | 70 ------------------- .../unary/success-citations-nolicense.json | 58 --------------- .../golden-files/unary/success-citations.json | 70 ------------------- .../unary/success-code-execution.json | 48 ------------- .../success-constraint-decoding-json.json | 34 --------- ...success-function-call-empty-arguments.json | 18 ----- .../success-function-call-json-literal.json | 45 ------------ .../unary/success-function-call-null.json | 45 ------------ .../unary/success-including-severity.json | 50 ------------- .../unary/success-partial-usage-metadata.json | 57 --------------- .../unary/success-quote-reply.json | 54 -------------- .../unary/success-unknown-enum.json | 52 -------------- .../unary/success-usage-metadata.json | 59 ---------------- 44 files changed, 7 insertions(+), 1200 deletions(-) delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json delete mode 100644 firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt index 4abf386765a..8b421edfa50 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt @@ -69,7 +69,7 @@ internal class StreamingSnapshotTests { @Test fun `unknown enum`() = - goldenStreamingFile("success-unknown-enum.txt") { + goldenStreamingFile("success-unknown-safety-enum.txt") { val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) withTimeout(testTimeout) { @@ -152,20 +152,6 @@ internal class StreamingSnapshotTests { } } - @Test - fun `citation returns correctly when using alternative name`() = - goldenStreamingFile("success-citations-altname.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.any { - it.candidates?.any { it.citationMetadata?.citationSources?.isNotEmpty() ?: false } - ?: false - } shouldBe true - } - } - @Test fun `stopped for recitation`() = goldenStreamingFile("failure-recitation-no-content.txt") { diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt index c316a9ece81..49a24201c3f 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt @@ -75,7 +75,7 @@ internal class UnarySnapshotTests { @Test fun `unknown enum`() = - goldenUnaryFile("success-unknown-enum.json") { + goldenUnaryFile("success-unknown-enum-safety-ratings.json") { withTimeout(testTimeout) { val response = apiController.generateContent(textGenerateContentRequest("prompt")) @@ -211,17 +211,6 @@ internal class UnarySnapshotTests { } } - @Test - fun `citation returns correctly when using alternative name`() = - goldenUnaryFile("success-citations-altname.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.citationMetadata?.citationSources?.isNotEmpty() shouldBe true - } - } - @OptIn(ExperimentalSerializationApi::class) @Test fun `properly translates json text`() = @@ -306,7 +295,7 @@ internal class UnarySnapshotTests { @Test fun `service disabled`() = - goldenUnaryFile("failure-service-disabled.json", HttpStatusCode.Forbidden) { + goldenUnaryFile("failure-firebaseml-api-not-enabled.json", HttpStatusCode.Forbidden) { withTimeout(testTimeout) { shouldThrow { apiController.generateContent(textGenerateContentRequest("prompt")) diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt index 5e52b1827b0..bf79df56604 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt @@ -140,7 +140,7 @@ internal fun goldenStreamingFile( httpStatusCode: HttpStatusCode = HttpStatusCode.OK, block: CommonTest, ) = doBlocking { - val goldenFile = loadGoldenFile("streaming/$name") + val goldenFile = loadGoldenFile("streaming-$name") val messages = goldenFile.readLines().filter { it.isNotBlank() } commonTest(httpStatusCode) { @@ -171,7 +171,7 @@ internal fun goldenUnaryFile( block: CommonTest, ) = commonTest(httpStatusCode) { - val goldenFile = loadGoldenFile("unary/$name") + val goldenFile = loadGoldenFile("unary-$name") val message = goldenFile.readText() channel.send(message.toByteArray()) @@ -186,7 +186,8 @@ internal fun goldenUnaryFile( * * @see goldenUnaryFile */ -internal fun loadGoldenFile(path: String): File = loadResourceFile("golden-files/$path") +internal fun loadGoldenFile(path: String): File = + loadResourceFile("vertexai-sdk-test-data/mock-responses/$path") /** Loads a file from the test resources directory. */ internal fun loadResourceFile(path: String) = File("src/test/resources/$path") diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt deleted file mode 100644 index ecf6f6b53fa..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-api-key.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "error": { - "code": 400, - "message": "API key not valid. Please pass a valid API key.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "API_KEY_INVALID", - "domain": "googleapis.com", - "metadata": { - "service": "generativelanguage.googleapis.com" - } - }, - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "Invalid API key: AIzv00G7VmUCUeC-5OglO3hcXM" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt deleted file mode 100644 index 5762b515325..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-empty-content.txt +++ /dev/null @@ -1 +0,0 @@ -data: {"candidates": [{"content": {},"index": 0}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt deleted file mode 100644 index 05e0936168d..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-finish-reason-safety.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": ""}],"role": "model"},"finishReason": "SAFETY","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt deleted file mode 100644 index 8c75fd7bf2d..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-http-error.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "$grpcMessage", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt deleted file mode 100644 index 8567086e2ec..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-image-rejected.txt +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error": { - "code": 400, - "message": "Request contains an invalid argument.", - "status": "INVALID_ARGUMENT" - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt deleted file mode 100644 index 58c914af08e..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-prompt-blocked-safety.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"promptFeedback": {"blockReason": "SAFETY","safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt deleted file mode 100644 index 6d69b64e51f..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-recitation-no-content.txt +++ /dev/null @@ -1,6 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "PLACEHOLDER"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "PLACEHOLDER"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "LOW"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 30,"endIndex": 179,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"finishReason": "RECITATION","index": 0}]} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt deleted file mode 100644 index 60b3f55c978..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/failure-unknown-model.txt +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 404, - "message": "models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.", - "status": "NOT_FOUND", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::not_found: models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.\" }" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt deleted file mode 100644 index 268f75d7821..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-long.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "**Cats:**\n\n1. **Anatomy and Appearance:**\n - Cats have"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " flexible bodies with a long tail, sharp retractable claws, and soft fur.\n - Their eyes are adapted for low-light conditions and have a vertical slit"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "-like pupil.\n - Cats come in a wide variety of breeds, each with distinct physical characteristics.\n\n2. **Behavior and Personality:**\n - Cats are known for their independence and solitary nature.\n - They are often described as aloof and mysterious, but they can also be affectionate and playful."}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "\n - Cats are territorial and communicate through body language, vocalizations, and scent marking.\n\n3. **Diet and Nutrition:**\n - Cats are obligate carnivores, meaning they require animal-based protein for survival.\n - Their diet should consist primarily of high-quality cat food that meets their nutritional needs.\n - Cats are prone to obesity, so portion control and regular exercise are important.\n\n4. **Health and Care:**\n - Cats require regular veterinary checkups, vaccinations, and parasite control.\n - They should be brushed regularly to prevent matting and shedding.\n - Providing"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " a clean litter box and fresh water is essential for their well-being.\n\n5. **Lifespan:**\n - The average lifespan of a cat is 12-15 years, although some cats can live longer with proper care.\n\n**Dogs:**\n\n1. **Anatomy and Appearance:**\n - Dogs have a diverse range of sizes, shapes, and coat types depending on their breed.\n - They have strong jaws with sharp teeth adapted for chewing and tearing.\n - Dogs' ears are typically floppy or erect and can be used to express emotions.\n\n2. **Behavior and Personality:**\n - Dogs are known for their loyalty, companionship, and trainability.\n - They are social animals that thrive on human interaction and form strong bonds with their owners.\n - Dogs communicate through barking, whining, growling, and body language.\n\n3. **Diet and Nutrition:**\n - Dogs are omnivores and can eat a variety of foods, including meat, grains, fruits, and vegetables.\n - Their diet should be balanced and meet their nutritional requirements based on age, size, and activity level.\n - Obesity is a common problem in dogs, so portion control and exercise are important.\n\n4"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": ". **Health and Care:**\n - Dogs require regular veterinary checkups, vaccinations, and parasite control.\n - They should be brushed regularly to maintain a healthy coat and prevent shedding.\n - Providing adequate exercise, mental stimulation, and socialization is essential for their well-being.\n\n5. **Lifespan:**\n - The average lifespan of a dog varies depending on breed, size, and overall health.\n - Smaller breeds tend to live longer than larger breeds, with an average lifespan of 10-15 years."}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt deleted file mode 100644 index b3c07628fc2..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-basic-reply-short.txt +++ /dev/null @@ -1,2 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "Cheyenne"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt deleted file mode 100644 index 4c682dc8f2f..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations-altname.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt deleted file mode 100644 index 3bb76e3d7a5..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-citations.txt +++ /dev/null @@ -1,12 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} - diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt deleted file mode 100644 index ef71be2906f..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-quotes-escaped.txt +++ /dev/null @@ -1,7 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": " Pineapples and \"bananas\" are two different types of fruit. Pineapples grow on a"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " tropical plant with a rosette of long, pointed leaves. Bananas grow on a herbaceous"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " plant with large, broad leaves. The two plants are not related, and pin"}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "eapples do not grow on banana plants."}]},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUAL","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DEROGATORY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS","probability": "NEGLIGIBLE"}]}]} diff --git a/firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt b/firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt deleted file mode 100644 index 0f3da8ebf17..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/streaming/success-unknown-enum.txt +++ /dev/null @@ -1,11 +0,0 @@ -data: {"candidates": [{"content": {"parts": [{"text": "**Cats:**\n\n- **Physical Characteristics:**\n - Size: Cats come"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - -data: {"candidates": [{"content": {"parts": [{"text": " in a wide range of sizes, from small breeds like the Singapura to large breeds like the Maine Coon.\n - Fur: Cats have soft, furry coats"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " that can vary in length and texture depending on the breed.\n - Eyes: Cats have large, expressive eyes that can be various colors, including green, blue, yellow, and hazel.\n - Ears: Cats have pointed, erect ears that are sensitive to sound.\n - Tail: Cats have long"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": ", flexible tails that they use for balance and communication.\n\n- **Behavior and Personality:**\n - Independent: Cats are often described as independent animals that enjoy spending time alone.\n - Affectionate: Despite their independent nature, cats can be very affectionate and form strong bonds with their owners.\n - Playful: Cats are naturally playful and enjoy engaging in activities such as chasing toys, climbing, and pouncing.\n - Curious: Cats are curious creatures that love to explore their surroundings.\n - Vocal: Cats communicate through a variety of vocalizations, including meows, purrs, hisses, and grow"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "ls.\n\n- **Health and Care:**\n - Diet: Cats are obligate carnivores, meaning they require animal-based protein for optimal health.\n - Grooming: Cats spend a significant amount of time grooming themselves to keep their fur clean and free of mats.\n - Exercise: Cats need regular exercise to stay healthy and active. This can be achieved through play sessions or access to outdoor space.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a cat's health and detecting any potential health issues early on.\n\n**Dogs:**\n\n- **Physical Characteristics:**\n - Size: Dogs come in a wide range of sizes, from small breeds like the Chihuahua to giant breeds like the Great Dane.\n - Fur: Dogs have fur coats that can vary in length, texture, and color depending on the breed.\n - Eyes: Dogs have expressive eyes that can be various colors, including brown, blue, green, and hazel.\n - Ears: Dogs have floppy or erect ears that are sensitive to sound.\n - Tail: Dogs have long, wagging tails that they use for communication and expressing emotions.\n\n- **Behavior and Personality:**\n - Loyal: Dogs are known for their loyalty and"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": " devotion to their owners.\n - Friendly: Dogs are generally friendly and outgoing animals that enjoy interacting with people and other animals.\n - Playful: Dogs are playful and energetic creatures that love to engage in activities such as fetching, running, and playing with toys.\n - Trainable: Dogs are highly trainable and can learn a variety of commands and tricks.\n - Vocal: Dogs communicate through a variety of vocalizations, including barking, howling, whining, and growling.\n\n- **Health and Care:**\n - Diet: Dogs are omnivores and can eat a variety of foods, including meat, vegetables, and grains.\n - Grooming: Dogs require regular grooming to keep their fur clean and free of mats. The frequency of grooming depends on the breed and coat type.\n - Exercise: Dogs need regular exercise to stay healthy and active. The amount of exercise required varies depending on the breed and age of the dog.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a dog's health and detecting any potential health issues early on."}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT_NEW_ENUM","probability": "NEGLIGIBLE_UNKNOWN_ENUM"}]}]} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json deleted file mode 100644 index ecf6f6b53fa..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-api-key.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "error": { - "code": 400, - "message": "API key not valid. Please pass a valid API key.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "API_KEY_INVALID", - "domain": "googleapis.com", - "metadata": { - "service": "generativelanguage.googleapis.com" - } - }, - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "Invalid API key: AIzv00G7VmUCUeC-5OglO3hcXM" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json deleted file mode 100644 index 4e1889660f2..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-empty-content.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "candidates": [ - { - "content": {}, - "index": 0 - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json deleted file mode 100644 index 111e33de2e3..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-finish-reason-safety.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "" - } - ], - "role": "model" - }, - "finishReason": "SAFETY", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "HIGH" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json deleted file mode 100644 index c8b07a5bf0e..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-http-error.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "$grpcMessage", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] -} -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json deleted file mode 100644 index 9dacdc71e7a..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-image-rejected.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "Request contains an invalid argument.", - "status": "INVALID_ARGUMENT", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json deleted file mode 100644 index 49d05e1840b..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-invalid-response.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "this": [ - { - "is": { - "not": [ - { - "a": "valid" - } - ] - }, - "response": {} - } - ] -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json deleted file mode 100644 index 737f2e08548..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-malformed-content.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "candidates": [ - { - "content": { - "invalid-field": true - }, - "index": 0 - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json deleted file mode 100644 index 9d2abbb23d6..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-prompt-blocked-safety.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "promptFeedback": { - "blockReason": "SAFETY", - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "HIGH" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json deleted file mode 100644 index fc438f7b080..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-quota-exceeded.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "error": { - "code": 429, - "message": "Quota exceeded for quota metric 'Generate Content API requests per minute' and limit 'GenerateContent request limit per minute for a region' of service 'generativelanguage.googleapis.com' for consumer 'project_number:348715329010'.", - "status": "RESOURCE_EXHAUSTED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "RATE_LIMIT_EXCEEDED", - "domain": "googleapis.com", - "metadata": { - "service": "generativelanguage.googleapis.com", - "consumer": "projects/348715329010", - "quota_limit_value": "0", - "quota_limit": "GenerateContentRequestsPerMinutePerProjectPerRegion", - "quota_location": "us-east2", - "quota_metric": "generativelanguage.googleapis.com/generate_content_requests" - } - }, - { - "@type": "type.googleapis.com/google.rpc.Help", - "links": [ - { - "description": "Request a higher quota limit.", - "url": "https://cloud.google.com/docs/quota#requesting_higher_quota" - } - ] - } - ] - } -} \ No newline at end of file diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json deleted file mode 100644 index ed842833aa5..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-service-disabled.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "error": { - "code": 403, - "message": "Firebase ML API has not been used in project 12345 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview?project=12345 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.Help", - "links": [ - { - "description": "Google developers console API activation", - "url": "https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview?project=12345" - } - ] - }, - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "SERVICE_DISABLED", - "domain": "googleapis.com", - "metadata": { - "service": "firebaseml.googleapis.com", - "consumer": "projects/12345" - } - } - ] - } -} \ No newline at end of file diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json deleted file mode 100644 index 60b3f55c978..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-unknown-model.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 404, - "message": "models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.", - "status": "NOT_FOUND", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::not_found: models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.\" }" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json b/firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json deleted file mode 100644 index c4c2ace4e20..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/failure-unsupported-user-location.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "User location is not supported for the API use.", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json deleted file mode 100644 index 2ee6617774a..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-long.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. **Use Freshly Ground Coffee**:\n - Grind your coffee beans just before brewing to preserve their flavor and aroma.\n - Use a burr grinder for a consistent grind size.\n\n2. **Choose the Right Water**:\n - Use filtered or spring water for the best taste.\n - Avoid using tap water, as it may contain impurities that can affect the flavor.\n\n3. **Measure Accurately**:\n - Use a kitchen scale to measure your coffee and water precisely.\n - A general rule of thumb is to use 1:16 ratio of coffee to water (e.g., 15 grams of coffee to 240 grams of water).\n\n4. **Preheat Your Equipment**:\n - Preheat your coffee maker or espresso machine before brewing to ensure a consistent temperature.\n\n5. **Control the Water Temperature**:\n - The ideal water temperature for brewing coffee is between 195°F (90°C) and 205°F (96°C).\n - Too hot water can extract bitter flavors, while too cold water won't extract enough flavor.\n\n6. **Steep the Coffee**:\n - For drip coffee, let the water slowly drip through the coffee grounds for optimal extraction.\n - For espresso, maintain a steady pressure and flow rate during the extraction.\n\n7. **Clean Your Equipment**:\n - Regularly clean your coffee maker or espresso machine to remove any residual oils or coffee grounds that can affect the taste.\n\n8. **Experiment with Different Coffee Beans**:\n - Try different coffee beans from various regions and roasts to find your preferred flavor profile.\n\n9. **Store Coffee Properly**:\n - Store your coffee beans in an airtight container in a cool, dark place to preserve their freshness.\n\n10. **Enjoy Freshly Brewed Coffee**:\n - Drink your coffee as soon as possible after brewing to savor its peak flavor and aroma." - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json deleted file mode 100644 index 40a9a6da58e..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-basic-reply-short.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "Mountain View, California, United States" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json deleted file mode 100644 index 7adaad5fcf2..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-altname.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "placeholder" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ], - "citationMetadata": { - "citations": [ - { - "startIndex": 574, - "endIndex": 705, - "uri": "https://example.com/", - "license": "" - }, - { - "startIndex": 899, - "endIndex": 1026, - "uri": "https://example.com/", - "license": "" - } - ] - } - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json deleted file mode 100644 index b8336b70ee4..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations-nolicense.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "Some information cited from an external source" - } - ] - }, - "finishReason": "STOP", - "safetyRatings": [ - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.16013464, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.074500255 - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.09687653, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.049313594 - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.16817278, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.09451043 - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.05023736, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.034553625 - } - ], - "citationMetadata": { - "citations": [ - { - "endIndex": 366, - "uri": "https://www.example.com/some-citation" - } - ] - } - } - ], - "usageMetadata": { - "promptTokenCount": 11, - "candidatesTokenCount": 135, - "totalTokenCount": 146 - } -} \ No newline at end of file diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json deleted file mode 100644 index 2a765aceda7..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-citations.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "placeholder" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ], - "citationMetadata": { - "citationSources": [ - { - "startIndex": 574, - "endIndex": 705, - "uri": "https://example.com/", - "license": "" - }, - { - "startIndex": 899, - "endIndex": 1026, - "uri": "https://example.com/", - "license": "" - } - ] - } - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json deleted file mode 100644 index 3b8f4c25f0e..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-code-execution.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "executableCode": { - "language": "PYTHON", - "code": "print(\"Hello World\")" - } - }, - { - "codeExecutionResult": { - "outcome": "OUTCOME_OK", - "output": "Hello World" - } - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 774, - "candidatesTokenCount": 4176, - "totalTokenCount": 4950 - } -} \ No newline at end of file diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json deleted file mode 100644 index 52c4a501823..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-constraint-decoding-json.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "[\n {\n \"name\": \"Fuji Dawn\",\n \"colors\": [\n \"#FEE4C4\",\n \"#FCDEC0\",\n \"#FBC7BB\",\n \"#F2A194\",\n \"#ED8571\"\n ]\n },\n {\n \"name\": \"Hawaiian Sunset\",\n \"colors\": [\n \"#F25C54\",\n \"#F24545\",\n \"#F22E35\",\n \"#C92127\",\n \"#96141A\"\n ]\n },\n {\n \"name\": \"Jakarta Noon\",\n \"colors\": [\n \"#037F8C\",\n \"#026773\",\n \"#014F59\",\n \"#F2C777\",\n \"#F2A857\"\n ]\n }\n]\n\n" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ] -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json deleted file mode 100644 index dc0b75ad930..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-empty-arguments.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "current_time" - } - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0 - } - ] -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json deleted file mode 100644 index fe4571880a8..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-json-literal.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "functionName", - "args": { - "original_title": "String", - "current": true - } - } - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 774, - "candidatesTokenCount": 4176, - "totalTokenCount": 4950 - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json deleted file mode 100644 index 14801eef9b8..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-function-call-null.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "functionName", - "args": { - "original_title": "String", - "season": null - } - } - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 774, - "candidatesTokenCount": 4176, - "totalTokenCount": 4950 - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json deleted file mode 100644 index ccaae7e3309..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-including-severity.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "candidates": [ - { - "content": { - "role": "model", - "parts": [ - { - "text": "## One Thousand and One Nights: A Summary\n\nOne Thousand and One Nights, also known as Arabian Nights, is a collection of Middle Eastern and South Asian stories." - } - ] - }, - "finishReason": "STOP", - "safetyRatings": [ - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.062331032, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.052134257 - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.04240383, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.06325052 - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.06359858, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.021990221 - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE", - "probabilityScore": 0.39030153, - "severity": "HARM_SEVERITY_NEGLIGIBLE", - "severityScore": 0.10650458 - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 11, - "candidatesTokenCount": 592, - "totalTokenCount": 603 - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json deleted file mode 100644 index 662b873f179..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-partial-usage-metadata.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "Mountain View, California, United States" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 6 - }, - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json deleted file mode 100644 index f1e5331ec3f..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-quote-reply.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. \"The greatest glory in living lies not in never falling, but in rising every time we fall.\" - Nelson Mandela\n2. \"The future belongs to those who believe in the beauty of their dreams.\" - Eleanor Roosevelt\n3. \"It does not matter how slow you go so long as you do not stop.\" - Confucius\n4. \"If you want to live a happy life, tie it to a goal, not to people or things.\" - Albert Einstein\n5. \"The only person you are destined to become is the person you decide to be.\" - Ralph Waldo Emerson\n6. \"It's not how much you have, but how much you enjoy that makes happiness.\" - Charles Spurgeon\n7. \"The greatest wealth is to live content with little.\" - Plato\n8. \"The only way to do great work is to love what you do.\" - Steve Jobs\n9. \"Don't be afraid to fail. Be afraid not to try.\" - Michael Jordan\n10. \"The best way to predict the future is to create it.\" - Abraham Lincoln" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json deleted file mode 100644 index b27a11ae955..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-unknown-enum.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "1. **Use Freshly Ground Coffee**:\n - Grind your coffee beans just before brewing to preserve their flavor and aroma.\n - Use a burr grinder for a consistent grind size.\n\n\n2. **Choose the Right Water**:\n - Use filtered or spring water for the best taste.\n - Avoid using tap water, as it may contain impurities that can affect the flavor.\n\n\n3. **Measure Accurately**:\n - Use a kitchen scale to measure your coffee and water precisely.\n - A general rule of thumb is to use 1:16 ratio of coffee to water (e.g., 15 grams of coffee to 240 grams of water).\n\n\n4. **Preheat Your Equipment**:\n - Preheat your coffee maker or espresso machine before brewing to ensure a consistent temperature.\n\n\n5. **Control the Water Temperature**:\n - The ideal water temperature for brewing coffee is between 195°F (90°C) and 205°F (96°C).\n - Too hot water can extract bitter flavors, while too cold water won't extract enough flavor.\n\n\n6. **Steep the Coffee**:\n - For drip coffee, let the water slowly drip through the coffee grounds for optimal extraction.\n - For pour-over coffee, pour the water in a circular motion over the coffee grounds, allowing it to steep for 30-45 seconds before continuing.\n\n\n7. **Clean Your Equipment**:\n - Regularly clean your coffee maker or espresso machine to prevent the buildup of oils and residue that can affect the taste of your coffee.\n\n\n8. **Experiment with Different Coffee Beans**:\n - Try different coffee beans from various regions and roasts to find your preferred flavor profile.\n - Experiment with different grind sizes and brewing methods to optimize the flavor of your chosen beans.\n\n\n9. **Store Coffee Properly**:\n - Store your coffee beans in an airtight container in a cool, dark place to preserve their freshness and flavor.\n - Avoid storing coffee in the refrigerator or freezer, as this can cause condensation and affect the taste.\n\n\n10. **Enjoy Freshly Brewed Coffee**:\n - Drink your coffee as soon as possible after brewing to enjoy its peak flavor and aroma.\n - Coffee starts to lose its flavor and aroma within 30 minutes of brewing." - } - ] - }, - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT_ENUM_NEW", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT_LIKE_A_NEW_ENUM", - "probability": "NEGLIGIBLE_NEW_ENUM" - } - ] - } -} diff --git a/firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json b/firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json deleted file mode 100644 index 77efea8f268..00000000000 --- a/firebase-vertexai/src/test/resources/golden-files/unary/success-usage-metadata.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "text": "Mountain View, California, United States" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "usageMetadata": { - "promptTokenCount": 6, - "candidatesTokenCount": 357, - "totalTokenCount": 363 - }, - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} From 512251bba06ee067380b2cc4a8fb61ef0431879b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:32:46 -0400 Subject: [PATCH 037/162] build(deps): bump app.cash.turbine:turbine from 1.0.0 to 1.2.0 (#6738) Bumps [app.cash.turbine:turbine](https://github.com/cashapp/turbine) from 1.0.0 to 1.2.0.
Release notes

Sourced from app.cash.turbine:turbine's releases.

1.2.0

Added

  • Add wasmWasi target.

1.1.0

Changed

  • Add wasmJs target, remove iosArm32 and watchosX86 targets.
  • Throw unconsumed events if scope is externally canceled.
Changelog

Sourced from app.cash.turbine:turbine's changelog.

1.2.0 - 2024-10-16

Added

  • Add wasmWasi target.

1.1.0 - 2024-03-06

Changed

  • Add wasmJs target, remove iosArm32 and watchosX86 targets.
  • Throw unconsumed events if scope is externally canceled.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=app.cash.turbine:turbine&package-manager=gradle&previous-version=1.0.0&new-version=1.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 489b504e75c..ed987b04a42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -207,7 +207,7 @@ spotless-plugin-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradl truth = { module = "com.google.truth:truth", version.ref = "truth" } truth-liteproto-extension = { module = "com.google.truth.extensions:truth-liteproto-extension", version.ref = "truth" } truth-proto-extension = { module = "com.google.truth.extensions:truth-proto-extension", version.ref = "truthProtoExtension" } -turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } +turbine = { module = "app.cash.turbine:turbine", version = "1.2.0" } # Remove three-ten-abp once minSdkVersion is changed to 26 or later, and, instead use the # correspondingly-named classes from the java.time package, which should be drop-in replacements. From 8f8a74d94c98208986291c273553c00f1950e81b Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 11 Mar 2025 16:02:33 -0600 Subject: [PATCH 038/162] Update changelogs and versions for Crashlytics, Perf, and AQS (#6758) Update the versions and changelog entries for Crashlytics, Perf, and AQS. The important entries are copied from AQS to Crashlytics so they get published on the release notes page. --- firebase-crashlytics-ndk/CHANGELOG.md | 2 +- firebase-crashlytics/CHANGELOG.md | 7 +++++-- firebase-perf/CHANGELOG.md | 4 ++-- firebase-sessions/CHANGELOG.md | 2 +- firebase-sessions/gradle.properties | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index 0cf17d1d025..237ce272b14 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [changed] Updated `firebase-crashlytics` dependency to v19.4.2 # 19.4.1 * [changed] Updated `firebase-crashlytics` dependency to v19.4.1 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 723e4a2e7d1..923fc710695 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased -* [changed] Internal changes to read version control info more efficiently [6754] -* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 [#6720] +* [changed] Internal changes to read version control info more efficiently [#6754] +* [fixed] Fixed NoSuchMethodError when getting process info on Android 13 on some devices [#6720] +* [changed] Updated `firebase-sessions` dependency to v2.1.0 + * [changed] Add warning for known issue [b/328687152](https://issuetracker.google.com/328687152) [#6755] + * [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8) [#6688] # 19.4.1 diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 9cfa4e6537a..69cb25eeae3 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -* [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] -* [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics. +* [changed] Updated `protolite-well-known-types` dependency to v18.0.1 [#6716] +* [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics [#6721] # 21.0.4 diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index eb5a6b85596..fb7165f1cfd 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased * [changed] Add warning for known issue b/328687152 * [changed] Use Dagger for dependency injection -* [changed] Updated datastore dependency to `1.1.3` to +* [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties index 6a74cb4445b..6faf5922ee1 100644 --- a/firebase-sessions/gradle.properties +++ b/firebase-sessions/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=2.0.10 +version=2.1.0 latestReleasedVersion=2.0.9 From bcdb963dca7abfa13d3824c0f1f0b8cd313089dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:00:20 -0400 Subject: [PATCH 039/162] build(deps): bump com.google.firebase:firebase-appdistribution-gradle from 5.0.0 to 5.1.1 (#6739) Bumps com.google.firebase:firebase-appdistribution-gradle from 5.0.0 to 5.1.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.firebase:firebase-appdistribution-gradle&package-manager=gradle&previous-version=5.0.0&new-version=5.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kai Bolay --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed987b04a42..5c191d08d81 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ dexmaker = "2.28.1" dexmakerVersion = "1.2" espressoCore = "3.6.1" featureDelivery = "2.1.0" -firebaseAppdistributionGradle = "5.0.0" +firebaseAppdistributionGradle = "5.1.1" firebaseCommon = "21.0.0" firebaseComponents = "18.0.0" firebaseCrashlyticsGradle = "3.0.2" From 17c302e200b7fffbc4c32bc0c9f81049241bce00 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:18:14 -0500 Subject: [PATCH 040/162] Adjust Create Release (#6761) We need explicit permissions to create PRs now, this adjusts our create release workflow to specify them. --- .github/workflows/create_releases.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/create_releases.yml b/.github/workflows/create_releases.yml index c47cfac9713..df4159cd285 100644 --- a/.github/workflows/create_releases.yml +++ b/.github/workflows/create_releases.yml @@ -15,6 +15,9 @@ on: jobs: create-branches: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: @@ -25,6 +28,9 @@ jobs: create-pull-request: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: actions/checkout@v4.1.1 with: From bdb330ec29680503e9e319b00d75ec99b96c42d6 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Thu, 13 Mar 2025 13:05:21 -0400 Subject: [PATCH 041/162] Add GenerationConfig to CountTokenRequest's (#6768) For reference b/402856353 --- firebase-vertexai/CHANGELOG.md | 3 +-- .../main/kotlin/com/google/firebase/vertexai/common/Request.kt | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index e28c285822e..863b520831f 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. # 16.2.0 * [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`. @@ -69,4 +69,3 @@ * [feature] Added support for `responseMimeType` in `GenerationConfig`. * [changed] Renamed `GoogleGenerativeAIException` to `FirebaseVertexAIException`. * [changed] Updated the KDocs for various classes and functions. - diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt index 7f84e053147..7b0bd65e0dc 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/Request.kt @@ -49,6 +49,7 @@ internal data class CountTokensRequest( val contents: List? = null, val tools: List? = null, @SerialName("system_instruction") val systemInstruction: Content.Internal? = null, + val generationConfig: GenerationConfig.Internal? = null ) : Request { companion object { @@ -58,6 +59,7 @@ internal data class CountTokensRequest( contents = generateContentRequest.contents, tools = generateContentRequest.tools, systemInstruction = generateContentRequest.systemInstruction, + generationConfig = generateContentRequest.generationConfig, ) } } From 19dd95bb556b7a00df2daf548ad50407345f1c0e Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Thu, 13 Mar 2025 13:21:35 -0400 Subject: [PATCH 042/162] [VertexAI] Remove redundant tests (#6762) There is an extra copy of some tests inside ` test/.../common` directory. This change removes them, and in the process: - Adds missing tests to the correct test file - Gets rid of duplicated tests and test-util code - Adds an exception type for quota exceeded --- firebase-vertexai/CHANGELOG.md | 2 + firebase-vertexai/api.txt | 3 + firebase-vertexai/gradle.properties | 2 +- .../firebase/vertexai/type/Exceptions.kt | 10 + .../firebase/vertexai/UnarySnapshotTests.kt | 36 ++ .../vertexai/common/StreamingSnapshotTests.kt | 190 ---------- .../vertexai/common/UnarySnapshotTests.kt | 353 ------------------ .../firebase/vertexai/common/util/tests.kt | 98 ----- 8 files changed, 52 insertions(+), 642 deletions(-) delete mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt delete mode 100644 firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index 863b520831f..62db530d71f 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased +* [changed] Added new exception type for quota exceeded scenarios. * [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. + # 16.2.0 * [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`. * [changed] Added support for modality-based token count. (#6658) diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt index ecf5ab8eefc..76491378d88 100644 --- a/firebase-vertexai/api.txt +++ b/firebase-vertexai/api.txt @@ -557,6 +557,9 @@ package com.google.firebase.vertexai.type { @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface PublicPreviewAPI { } + public final class QuotaExceededException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { + } + public final class RequestOptions { ctor public RequestOptions(); ctor public RequestOptions(long timeoutInMillis = 180.seconds.inWholeMilliseconds); diff --git a/firebase-vertexai/gradle.properties b/firebase-vertexai/gradle.properties index 546c015493e..c0a96853e52 100644 --- a/firebase-vertexai/gradle.properties +++ b/firebase-vertexai/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.2.1 +version=16.3.0 latestReleasedVersion=16.2.0 diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt index 4890cd7ada3..4a29e5c37ea 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt @@ -59,6 +59,8 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti UnknownException(cause.message ?: "", cause.cause) is com.google.firebase.vertexai.common.ContentBlockedException -> ContentBlockedException(cause.message ?: "", cause.cause) + is com.google.firebase.vertexai.common.QuotaExceededException -> + QuotaExceededException(cause.message ?: "", cause.cause) else -> UnknownException(cause.message ?: "", cause) } is TimeoutCancellationException -> @@ -165,6 +167,14 @@ public class ServiceDisabledException internal constructor(message: String, cause: Throwable? = null) : FirebaseVertexAIException(message, cause) +/** + * The request has hit a quota limit. Learn more about quotas in the + * [Firebase documentation.](https://firebase.google.com/docs/vertex-ai/quotas) + */ +public class QuotaExceededException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseVertexAIException(message, cause) + /** Catch all case for exceptions not explicitly expected. */ public class UnknownException internal constructor(message: String, cause: Throwable? = null) : FirebaseVertexAIException(message, cause) diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt index 1724b3788cb..a7ed1c4ed7f 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt @@ -27,6 +27,7 @@ import com.google.firebase.vertexai.type.HarmSeverity import com.google.firebase.vertexai.type.InvalidAPIKeyException import com.google.firebase.vertexai.type.PromptBlockedException import com.google.firebase.vertexai.type.PublicPreviewAPI +import com.google.firebase.vertexai.type.QuotaExceededException import com.google.firebase.vertexai.type.ResponseStoppedException import com.google.firebase.vertexai.type.SerializationException import com.google.firebase.vertexai.type.ServerException @@ -72,6 +73,19 @@ internal class UnarySnapshotTests { } } + @Test + fun `long reply`() = + goldenUnaryFile("unary-success-basic-reply-long.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().safetyRatings.isEmpty() shouldBe false + } + } + @Test fun `response with detailed token-based usageMetadata`() = goldenUnaryFile("unary-success-basic-response-long-usage-metadata.json") { @@ -177,6 +191,20 @@ internal class UnarySnapshotTests { } } + @Test + fun `function call has no arguments field`() = + goldenUnaryFile("unary-success-function-call-empty-arguments.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + val content = response.candidates.shouldNotBeNullOrEmpty().first().content + content.shouldNotBeNull() + val callPart = content.parts.shouldNotBeNullOrEmpty().first() as FunctionCallPart + + callPart.name shouldBe "current_time" + callPart.args shouldBe emptyMap() + } + } + @Test fun `prompt blocked for safety`() = goldenUnaryFile("unary-failure-prompt-blocked-safety.json") { @@ -239,6 +267,14 @@ internal class UnarySnapshotTests { } } + @Test + fun `quota exceeded`() = + goldenUnaryFile("unary-failure-quota-exceeded.json", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + @Test fun `stopped for safety with no content`() = goldenUnaryFile("unary-failure-finish-reason-safety-no-content.json") { diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt deleted file mode 100644 index 8b421edfa50..00000000000 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/StreamingSnapshotTests.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.vertexai.common - -import com.google.firebase.vertexai.common.util.goldenStreamingFile -import com.google.firebase.vertexai.type.BlockReason -import com.google.firebase.vertexai.type.FinishReason -import com.google.firebase.vertexai.type.HarmCategory -import com.google.firebase.vertexai.type.TextPart -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import io.ktor.http.HttpStatusCode -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi -import org.junit.Test - -@OptIn(ExperimentalSerializationApi::class) -internal class StreamingSnapshotTests { - private val testTimeout = 5.seconds - - @Test - fun `short reply`() = - goldenStreamingFile("success-basic-reply-short.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.isEmpty() shouldBe false - responseList.first().candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - responseList.first().candidates?.first()?.content?.parts?.isEmpty() shouldBe false - responseList.first().candidates?.first()?.safetyRatings?.isEmpty() shouldBe false - } - } - - @Test - fun `long reply`() = - goldenStreamingFile("success-basic-reply-long.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.isEmpty() shouldBe false - responseList.forEach { - it.candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - it.candidates?.first()?.content?.parts?.isEmpty() shouldBe false - it.candidates?.first()?.safetyRatings?.isEmpty() shouldBe false - } - } - } - - @Test - fun `unknown enum`() = - goldenStreamingFile("success-unknown-safety-enum.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.isEmpty() shouldBe false - responseList.any { - it.candidates?.any { - it.safetyRatings?.any { it.category == HarmCategory.Internal.UNKNOWN } ?: false - } - ?: false - } shouldBe true - } - } - - @Test - fun `quotes escaped`() = - goldenStreamingFile("success-quotes-escaped.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - - responseList.isEmpty() shouldBe false - val part = - responseList.first().candidates?.first()?.content?.parts?.first() as? TextPart.Internal - part.shouldNotBeNull() - part.text shouldContain "\"" - } - } - - @Test - fun `prompt blocked for safety`() = - goldenStreamingFile("failure-prompt-blocked-safety.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response?.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY - } - } - - @Test - fun `empty content`() = - goldenStreamingFile("failure-empty-content.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `http errors`() = - goldenStreamingFile("failure-http-error.txt", HttpStatusCode.PreconditionFailed) { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `stopped for safety`() = - goldenStreamingFile("failure-finish-reason-safety.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.SAFETY - } - } - - @Test - fun `citation parsed correctly`() = - goldenStreamingFile("success-citations.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val responseList = responses.toList() - responseList.any { - it.candidates?.any { it.citationMetadata?.citationSources?.isNotEmpty() ?: false } - ?: false - } shouldBe true - } - } - - @Test - fun `stopped for recitation`() = - goldenStreamingFile("failure-recitation-no-content.txt") { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { - val exception = shouldThrow { responses.collect() } - exception.response.candidates?.first()?.finishReason shouldBe - FinishReason.Internal.RECITATION - } - } - - @Test - fun `image rejected`() = - goldenStreamingFile("failure-image-rejected.txt", HttpStatusCode.BadRequest) { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `unknown model`() = - goldenStreamingFile("failure-unknown-model.txt", HttpStatusCode.NotFound) { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } - - @Test - fun `invalid api key`() = - goldenStreamingFile("failure-api-key.txt", HttpStatusCode.BadRequest) { - val responses = apiController.generateContentStream(textGenerateContentRequest("prompt")) - - withTimeout(testTimeout) { shouldThrow { responses.collect() } } - } -} diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt deleted file mode 100644 index 49a24201c3f..00000000000 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/UnarySnapshotTests.kt +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.vertexai.common - -import com.google.firebase.vertexai.common.util.goldenUnaryFile -import com.google.firebase.vertexai.common.util.shouldNotBeNullOrEmpty -import com.google.firebase.vertexai.type.BlockReason -import com.google.firebase.vertexai.type.FinishReason -import com.google.firebase.vertexai.type.FunctionCallPart -import com.google.firebase.vertexai.type.HarmCategory -import com.google.firebase.vertexai.type.HarmProbability -import com.google.firebase.vertexai.type.HarmSeverity -import com.google.firebase.vertexai.type.TextPart -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.collections.shouldNotBeEmpty -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.should -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import io.ktor.http.HttpStatusCode -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonPrimitive -import org.junit.Test - -@Serializable internal data class MountainColors(val name: String, val colors: List) - -internal class UnarySnapshotTests { - private val testTimeout = 5.seconds - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `short reply`() = - goldenUnaryFile("success-basic-reply-short.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - response.candidates?.first()?.content?.parts?.isEmpty() shouldBe false - response.candidates?.first()?.safetyRatings?.isEmpty() shouldBe false - } - } - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `long reply`() = - goldenUnaryFile("success-basic-reply-long.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - response.candidates?.first()?.content?.parts?.isEmpty() shouldBe false - response.candidates?.first()?.safetyRatings?.isEmpty() shouldBe false - } - } - - @Test - fun `unknown enum`() = - goldenUnaryFile("success-unknown-enum-safety-ratings.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isNullOrEmpty() shouldBe false - val candidate = response.candidates?.first() - candidate?.safetyRatings?.any { it.category == HarmCategory.Internal.UNKNOWN } shouldBe true - } - } - - @Test - fun `safetyRatings including severity`() = - goldenUnaryFile("success-including-severity.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.safetyRatings?.isEmpty() shouldBe false - response.candidates?.first()?.safetyRatings?.all { - it.probability == HarmProbability.Internal.NEGLIGIBLE - } shouldBe true - response.candidates?.first()?.safetyRatings?.all { it.probabilityScore != null } shouldBe - true - response.candidates?.first()?.safetyRatings?.all { - it.severity == HarmSeverity.Internal.NEGLIGIBLE - } shouldBe true - response.candidates?.first()?.safetyRatings?.all { it.severityScore != null } shouldBe true - } - } - - @Test - fun `prompt blocked for safety`() = - goldenUnaryFile("failure-prompt-blocked-safety.json") { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } should { it.response?.promptFeedback?.blockReason shouldBe BlockReason.Internal.SAFETY } - } - } - - @Test - fun `empty content`() = - goldenUnaryFile("failure-empty-content.json") { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `http error`() = - goldenUnaryFile("failure-http-error.json", HttpStatusCode.PreconditionFailed) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `user location error`() = - goldenUnaryFile("failure-unsupported-user-location.json", HttpStatusCode.PreconditionFailed) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `stopped for safety`() = - goldenUnaryFile("failure-finish-reason-safety.json") { - withTimeout(testTimeout) { - val exception = - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - exception.response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.SAFETY - } - } - - @Test - fun `citation returns correctly`() = - goldenUnaryFile("success-citations.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.citationMetadata?.citationSources?.isNotEmpty() shouldBe true - } - } - - @Test - fun `citation returns correctly with missing license and startIndex`() = - goldenUnaryFile("success-citations-nolicense.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.citationMetadata?.citationSources?.isNotEmpty() shouldBe true - // Verify the values in the citation source - with(response.candidates?.first()?.citationMetadata?.citationSources?.first()!!) { - license shouldBe null - startIndex shouldBe 0 - } - } - } - - @Test - fun `response includes usage metadata`() = - goldenUnaryFile("success-usage-metadata.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - response.usageMetadata shouldNotBe null - response.usageMetadata?.totalTokenCount shouldBe 363 - } - } - - @Test - fun `response includes partial usage metadata`() = - goldenUnaryFile("success-partial-usage-metadata.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - response.candidates?.first()?.finishReason shouldBe FinishReason.Internal.STOP - response.usageMetadata shouldNotBe null - response.usageMetadata?.promptTokenCount shouldBe 6 - response.usageMetadata?.totalTokenCount shouldBe null - } - } - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `properly translates json text`() = - goldenUnaryFile("success-constraint-decoding-json.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - - response.candidates?.isEmpty() shouldBe false - with( - response.candidates - ?.first() - ?.content - ?.parts - ?.first() - ?.shouldBeInstanceOf() - ) { - shouldNotBeNull() - JSON.decodeFromString>(text).shouldNotBeEmpty() - } - } - } - - @Test - fun `invalid response`() = - goldenUnaryFile("failure-invalid-response.json") { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `malformed content`() = - goldenUnaryFile("failure-malformed-content.json") { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `invalid api key`() = - goldenUnaryFile("failure-api-key.json", HttpStatusCode.BadRequest) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `quota exceeded`() = - goldenUnaryFile("failure-quota-exceeded.json", HttpStatusCode.BadRequest) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `image rejected`() = - goldenUnaryFile("failure-image-rejected.json", HttpStatusCode.BadRequest) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `unknown model`() = - goldenUnaryFile("failure-unknown-model.json", HttpStatusCode.NotFound) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @Test - fun `service disabled`() = - goldenUnaryFile("failure-firebaseml-api-not-enabled.json", HttpStatusCode.Forbidden) { - withTimeout(testTimeout) { - shouldThrow { - apiController.generateContent(textGenerateContentRequest("prompt")) - } - } - } - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `function call contains null param`() = - goldenUnaryFile("success-function-call-null.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - val callPart = - (response.candidates!!.first().content!!.parts.first() as FunctionCallPart.Internal) - - callPart.functionCall.args shouldNotBe null - callPart.functionCall.args?.get("season") shouldBe null - } - } - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `function call contains json literal`() = - goldenUnaryFile("success-function-call-json-literal.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - val content = response.candidates.shouldNotBeNullOrEmpty().first().content - val callPart = - content.let { - it.shouldNotBeNull() - it.parts.shouldNotBeEmpty() - it.parts.first().shouldBeInstanceOf() - } - - callPart.functionCall.args shouldNotBe null - callPart.functionCall.args?.get("current") shouldBe JsonPrimitive(true) - } - } - - @OptIn(ExperimentalSerializationApi::class) - @Test - fun `function call has no arguments field`() = - goldenUnaryFile("success-function-call-empty-arguments.json") { - withTimeout(testTimeout) { - val response = apiController.generateContent(textGenerateContentRequest("prompt")) - val content = response.candidates.shouldNotBeNullOrEmpty().first().content - content.shouldNotBeNull() - val callPart = content.parts.shouldNotBeNullOrEmpty().first() as FunctionCallPart.Internal - - callPart.functionCall.name shouldBe "current_time" - callPart.functionCall.args shouldBe null - } - } -} diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt index bf79df56604..855c8aa4a8b 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt @@ -19,25 +19,18 @@ package com.google.firebase.vertexai.common.util import com.google.firebase.vertexai.common.APIController -import com.google.firebase.vertexai.common.GenerateContentRequest import com.google.firebase.vertexai.common.JSON import com.google.firebase.vertexai.type.Candidate import com.google.firebase.vertexai.type.Content import com.google.firebase.vertexai.type.GenerateContentResponse import com.google.firebase.vertexai.type.RequestOptions import com.google.firebase.vertexai.type.TextPart -import io.kotest.matchers.collections.shouldNotBeEmpty -import io.kotest.matchers.nulls.shouldNotBeNull import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf import io.ktor.utils.io.ByteChannel -import io.ktor.utils.io.close -import io.ktor.utils.io.writeFully -import java.io.File -import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString @@ -47,18 +40,6 @@ internal fun prepareStreamingResponse( response: List ): List = response.map { "data: ${JSON.encodeToString(it)}$SSE_SEPARATOR".toByteArray() } -internal fun prepareResponse(response: GenerateContentResponse.Internal) = - JSON.encodeToString(response).toByteArray() - -@OptIn(ExperimentalSerializationApi::class) -internal fun createRequest(vararg text: String): GenerateContentRequest { - val contents = text.map { Content.Internal(parts = listOf(TextPart.Internal(it))) } - - return GenerateContentRequest("gemini", contents) -} - -internal fun createResponse(text: String) = createResponses(text).single() - @OptIn(ExperimentalSerializationApi::class) internal fun createResponses(vararg text: String): List { val candidates = @@ -123,82 +104,3 @@ internal fun commonTest( ) CommonTestScope(channel, apiController).block() } - -/** - * A variant of [commonTest] for performing *streaming-based* snapshot tests. - * - * Loads the *Golden File* and automatically parses the messages from it; providing it to the - * channel. - * - * @param name The name of the *Golden File* to load - * @param httpStatusCode An optional [HttpStatusCode] to return as a response - * @param block The test contents themselves, with a [CommonTestScope] implicitly provided - * @see goldenUnaryFile - */ -internal fun goldenStreamingFile( - name: String, - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - block: CommonTest, -) = doBlocking { - val goldenFile = loadGoldenFile("streaming-$name") - val messages = goldenFile.readLines().filter { it.isNotBlank() } - - commonTest(httpStatusCode) { - launch { - for (message in messages) { - channel.writeFully("$message$SSE_SEPARATOR".toByteArray()) - } - channel.close() - } - - block() - } -} - -/** - * A variant of [commonTest] for performing snapshot tests. - * - * Loads the *Golden File* and automatically provides it to the channel. - * - * @param name The name of the *Golden File* to load - * @param httpStatusCode An optional [HttpStatusCode] to return as a response - * @param block The test contents themselves, with a [CommonTestScope] implicitly provided - * @see goldenStreamingFile - */ -internal fun goldenUnaryFile( - name: String, - httpStatusCode: HttpStatusCode = HttpStatusCode.OK, - block: CommonTest, -) = - commonTest(httpStatusCode) { - val goldenFile = loadGoldenFile("unary-$name") - val message = goldenFile.readText() - - channel.send(message.toByteArray()) - - block() - } - -/** - * Loads a *Golden File* from the resource directory. - * - * Expects golden files to live under `golden-files` in the resource files. - * - * @see goldenUnaryFile - */ -internal fun loadGoldenFile(path: String): File = - loadResourceFile("vertexai-sdk-test-data/mock-responses/$path") - -/** Loads a file from the test resources directory. */ -internal fun loadResourceFile(path: String) = File("src/test/resources/$path") - -/** - * Ensures that a collection is neither null or empty. - * - * Syntax sugar for [shouldNotBeNull] and [shouldNotBeEmpty]. - */ -inline fun Collection?.shouldNotBeNullOrEmpty(): Collection { - shouldNotBeNull() - shouldNotBeEmpty() - return this -} From b9be9f1ca0ebd0a8a67b06c282b0f83888145651 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:15:59 -0500 Subject: [PATCH 043/162] Add m161 changelog for functions (#6769) --- firebase-functions/CHANGELOG.md | 1 + firebase-functions/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 785f0f9966a..7b0f4e03b6d 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [feature] Streaming callable functions are now supported. * [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error. diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index 6fe83923849..4e8c15934dd 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=21.1.2 +version=21.2.0 latestReleasedVersion=21.1.1 android.enableUnitTestBinaryResources=true From f7e98b83e5939f7f3792c5507211db4651dc5b86 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 14 Mar 2025 15:11:30 -0400 Subject: [PATCH 044/162] dataconnect: demo: upgrade versions in build.gradle.kts (#6776) --- firebase-dataconnect/demo/build.gradle.kts | 20 +++++++++---------- .../dataconnect/minimaldemo/MyApplication.kt | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/firebase-dataconnect/demo/build.gradle.kts b/firebase-dataconnect/demo/build.gradle.kts index 78465a0df41..add5ebb090a 100644 --- a/firebase-dataconnect/demo/build.gradle.kts +++ b/firebase-dataconnect/demo/build.gradle.kts @@ -19,12 +19,12 @@ import java.nio.charset.StandardCharsets plugins { // Use whichever versions of these dependencies suit your application. - // The versions shown here were the latest versions as of December 03, 2024. + // The versions shown here were the latest versions as of March 05, 2025. // Note, however, that the version of kotlin("plugin.serialization") _must_, // in general, match the version of kotlin("android"). - id("com.android.application") version "8.7.3" + id("com.android.application") version "8.9.0" id("com.google.gms.google-services") version "4.4.2" - val kotlinVersion = "2.1.0" + val kotlinVersion = "2.1.10" kotlin("android") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion @@ -35,19 +35,19 @@ plugins { dependencies { // Use whichever versions of these dependencies suit your application. - // The versions shown here were the latest versions as of December 03, 2024. - implementation("com.google.firebase:firebase-dataconnect:16.0.0-beta03") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3") + // The versions shown here were the latest versions as of March 05, 2025. + implementation("com.google.firebase:firebase-dataconnect:16.0.0-beta04") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0") implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.activity:activity-ktx:1.9.3") + implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") implementation("com.google.android.material:material:1.12.0") // The following code in this "dependencies" block can be omitted from customer // facing documentation as it is an implementation detail of this application. - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") implementation("io.kotest:kotest-property:5.9.1") implementation("io.kotest.extensions:kotest-property-arbs:2.1.2") } diff --git a/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt b/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt index eb70e8af475..1b6360efb58 100644 --- a/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt +++ b/firebase-dataconnect/demo/src/main/kotlin/com/google/firebase/dataconnect/minimaldemo/MyApplication.kt @@ -61,7 +61,7 @@ class MyApplication : Application() { } ) - private val initialLogLevel = FirebaseDataConnect.logLevel + private val initialLogLevel = FirebaseDataConnect.logLevel.value private val connectorMutex = Mutex() private var connector: Ctry3q3tp6kzxConnector? = null @@ -70,7 +70,7 @@ class MyApplication : Application() { coroutineScope.launch { if (getDataConnectDebugLoggingEnabled()) { - FirebaseDataConnect.logLevel = LogLevel.DEBUG + FirebaseDataConnect.logLevel.value = LogLevel.DEBUG } } } @@ -102,7 +102,7 @@ class MyApplication : Application() { getSharedPreferences().all[SharedPrefsKeys.IS_DATA_CONNECT_LOGGING_ENABLED] as? Boolean ?: false suspend fun setDataConnectDebugLoggingEnabled(enabled: Boolean) { - FirebaseDataConnect.logLevel = if (enabled) LogLevel.DEBUG else initialLogLevel + FirebaseDataConnect.logLevel.value = if (enabled) LogLevel.DEBUG else initialLogLevel editSharedPreferences { putBoolean(SharedPrefsKeys.IS_DATA_CONNECT_LOGGING_ENABLED, enabled) } } From 1aca8994cc4f1e3107f1f7cc9fcd8f03d39a79af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?= Date: Fri, 14 Mar 2025 21:09:59 +0000 Subject: [PATCH 045/162] chore(functions): export reactive-streams as a transitive dependency (#6775) Alternative to #6774 - we'll keep that PR for a follow-up release. For now, we're exposing the `reactive-streams` dependency to make sure it works out of the box for Java developers. --- firebase-functions/firebase-functions.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 08a797112b9..b1c220a6594 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -112,7 +112,7 @@ dependencies { implementation(libs.okhttp) implementation(libs.playservices.base) implementation(libs.playservices.basement) - implementation(libs.reactive.streams) + api(libs.reactive.streams) api(libs.playservices.tasks) From 0554f0df8b8c8972c9497bf16aa861a2e42e5dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?= Date: Fri, 14 Mar 2025 21:10:16 +0000 Subject: [PATCH 046/162] fix(functions): use notifyError() instead of throwing (#6773) Throwing inside the `PublisherStream` causes a Runtime exception that can't be caught in the call site. Instead, we should use `notifyError()` so that the error can be caught in the `Subscriber#onError()` function. I tried to catch the exception using both of the code snippets below, but none of them worked. (they both work with the changes in this PR): ```kotlin functions.getHttpsCallable("nonExistentFunction") .stream().asFlow() .catch { // Handle error for a 404 function } .collect { // ... } ``` ```kotlin try { functions.getHttpsCallable("nonExistentFunction") .stream().asFlow() .collect { // ... } } catch(e: Exception) { // Handle error for a 404 function } ``` Runtime exception thrown: ``` FATAL EXCEPTION: OkHttp Dispatcher Process: com.google.samples.quickstart.functions, PID: 13321 com.google.firebase.functions.FirebaseFunctionsException: Value of type java.lang.String cannot be converted to JSONObject Unexpected Response: 404 Page not found

Error: Page not found

The requested URL was not found on this server.

at com.google.firebase.functions.PublisherStream.validateResponse(PublisherStream.kt:316) at com.google.firebase.functions.PublisherStream.access$validateResponse(PublisherStream.kt:41) at com.google.firebase.functions.PublisherStream$startStreaming$1$4.onResponse(PublisherStream.kt:161) at okhttp3.RealCall$AsyncCall.execute(RealCall.java:203) at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651) at java.lang.Thread.run(Thread.java:1119) ``` --- .../google/firebase/functions/StreamTests.kt | 20 ++++++++++++++ .../firebase/functions/PublisherStream.kt | 27 ++++++++++--------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt index e0de5cc2262..300385f6a13 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -143,6 +143,26 @@ class StreamTests { assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java) } + @Test + fun nonExistentFunction_receivesError() = runBlocking { + val function = + functions.getHttpsCallable("nonexistentFunction").withTimeout(2000, TimeUnit.MILLISECONDS) + val subscriber = StreamSubscriber() + + function.stream().subscribe(subscriber) + + withTimeout(2000) { + while (subscriber.throwable == null) { + delay(100) + } + } + + assertThat(subscriber.throwable).isNotNull() + assertThat(subscriber.throwable).isInstanceOf(FirebaseFunctionsException::class.java) + assertThat((subscriber.throwable as FirebaseFunctionsException).code) + .isEqualTo(FirebaseFunctionsException.Code.NOT_FOUND) + } + @Test fun genStreamWeather_receivesWeatherForecasts() = runBlocking { val inputData = listOf(mapOf("name" to "Toronto"), mapOf("name" to "London")) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt index 6fc6a9d657c..a8ef77c9442 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt @@ -300,10 +300,12 @@ internal class PublisherStream( val errorMessage: String if (response.code() == 404 && response.header("Content-Type") == htmlContentType) { errorMessage = """URL not found. Raw response: ${response.body()?.string()}""".trimMargin() - throw FirebaseFunctionsException( - errorMessage, - FirebaseFunctionsException.Code.fromHttpStatus(response.code()), - null + notifyError( + FirebaseFunctionsException( + errorMessage, + FirebaseFunctionsException.Code.fromHttpStatus(response.code()), + null + ) ) } @@ -313,16 +315,17 @@ internal class PublisherStream( val json = JSONObject(text) error = serializer.decode(json.opt("error")) } catch (e: Throwable) { - throw FirebaseFunctionsException( - "${e.message} Unexpected Response:\n$text ", - FirebaseFunctionsException.Code.INTERNAL, - e + notifyError( + FirebaseFunctionsException( + "${e.message} Unexpected Response:\n$text ", + FirebaseFunctionsException.Code.INTERNAL, + e + ) ) + return } - throw FirebaseFunctionsException( - error.toString(), - FirebaseFunctionsException.Code.INTERNAL, - error + notifyError( + FirebaseFunctionsException(error.toString(), FirebaseFunctionsException.Code.INTERNAL, error) ) } } From 9b42d841540cbf2eb52e0e8cdaff6a131dc2eaf4 Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Mon, 17 Mar 2025 23:07:52 +0000 Subject: [PATCH 047/162] fix for new version of golden files (#6771) Co-authored-by: David Motsonashvili --- ...s.kt => VertexAIStreamingSnapshotTests.kt} | 42 +++++---- ...Tests.kt => VertexAIUnarySnapshotTests.kt} | 91 ++++++++++--------- .../google/firebase/vertexai/util/tests.kt | 34 ++++++- firebase-vertexai/update_responses.sh | 2 +- 4 files changed, 103 insertions(+), 66 deletions(-) rename firebase-vertexai/src/test/java/com/google/firebase/vertexai/{StreamingSnapshotTests.kt => VertexAIStreamingSnapshotTests.kt} (82%) rename firebase-vertexai/src/test/java/com/google/firebase/vertexai/{UnarySnapshotTests.kt => VertexAIUnarySnapshotTests.kt} (84%) diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIStreamingSnapshotTests.kt similarity index 82% rename from firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt rename to firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIStreamingSnapshotTests.kt index ce53bcf9e33..981144a8e14 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/StreamingSnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIStreamingSnapshotTests.kt @@ -25,7 +25,7 @@ import com.google.firebase.vertexai.type.ResponseStoppedException import com.google.firebase.vertexai.type.SerializationException import com.google.firebase.vertexai.type.ServerException import com.google.firebase.vertexai.type.TextPart -import com.google.firebase.vertexai.util.goldenStreamingFile +import com.google.firebase.vertexai.util.goldenVertexStreamingFile import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -33,17 +33,16 @@ import io.kotest.matchers.string.shouldContain import io.ktor.http.HttpStatusCode import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withTimeout import org.junit.Test -internal class StreamingSnapshotTests { +internal class VertexAIStreamingSnapshotTests { private val testTimeout = 5.seconds @Test fun `short reply`() = - goldenStreamingFile("streaming-success-basic-reply-short.txt") { + goldenVertexStreamingFile("streaming-success-basic-reply-short.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -57,7 +56,7 @@ internal class StreamingSnapshotTests { @Test fun `long reply`() = - goldenStreamingFile("streaming-success-basic-reply-long.txt") { + goldenVertexStreamingFile("streaming-success-basic-reply-long.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -73,7 +72,7 @@ internal class StreamingSnapshotTests { @Test fun `unknown enum in safety ratings`() = - goldenStreamingFile("streaming-success-unknown-safety-enum.txt") { + goldenVertexStreamingFile("streaming-success-unknown-safety-enum.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -88,7 +87,7 @@ internal class StreamingSnapshotTests { @Test fun `unknown enum in finish reason`() = - goldenStreamingFile("streaming-failure-unknown-finish-enum.txt") { + goldenVertexStreamingFile("streaming-failure-unknown-finish-enum.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -99,7 +98,7 @@ internal class StreamingSnapshotTests { @Test fun `quotes escaped`() = - goldenStreamingFile("streaming-success-quotes-escaped.txt") { + goldenVertexStreamingFile("streaming-success-quotes-escaped.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -114,7 +113,7 @@ internal class StreamingSnapshotTests { @Test fun `prompt blocked for safety`() = - goldenStreamingFile("streaming-failure-prompt-blocked-safety.txt") { + goldenVertexStreamingFile("streaming-failure-prompt-blocked-safety.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -125,7 +124,7 @@ internal class StreamingSnapshotTests { @Test fun `prompt blocked for safety with message`() = - goldenStreamingFile("streaming-failure-prompt-blocked-safety-with-message.txt") { + goldenVertexStreamingFile("streaming-failure-prompt-blocked-safety-with-message.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -137,7 +136,7 @@ internal class StreamingSnapshotTests { @Test fun `empty content`() = - goldenStreamingFile("streaming-failure-empty-content.txt") { + goldenVertexStreamingFile("streaming-failure-empty-content.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -145,7 +144,10 @@ internal class StreamingSnapshotTests { @Test fun `http errors`() = - goldenStreamingFile("streaming-failure-http-error.txt", HttpStatusCode.PreconditionFailed) { + goldenVertexStreamingFile( + "streaming-failure-http-error.txt", + HttpStatusCode.PreconditionFailed + ) { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -153,7 +155,7 @@ internal class StreamingSnapshotTests { @Test fun `stopped for safety`() = - goldenStreamingFile("streaming-failure-finish-reason-safety.txt") { + goldenVertexStreamingFile("streaming-failure-finish-reason-safety.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -164,7 +166,7 @@ internal class StreamingSnapshotTests { @Test fun `citation parsed correctly`() = - goldenStreamingFile("streaming-success-citations.txt") { + goldenVertexStreamingFile("streaming-success-citations.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -177,7 +179,7 @@ internal class StreamingSnapshotTests { @Test fun `stopped for recitation`() = - goldenStreamingFile("streaming-failure-recitation-no-content.txt") { + goldenVertexStreamingFile("streaming-failure-recitation-no-content.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { @@ -188,7 +190,7 @@ internal class StreamingSnapshotTests { @Test fun `image rejected`() = - goldenStreamingFile("streaming-failure-image-rejected.txt", HttpStatusCode.BadRequest) { + goldenVertexStreamingFile("streaming-failure-image-rejected.txt", HttpStatusCode.BadRequest) { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -196,7 +198,7 @@ internal class StreamingSnapshotTests { @Test fun `unknown model`() = - goldenStreamingFile("streaming-failure-unknown-model.txt", HttpStatusCode.NotFound) { + goldenVertexStreamingFile("streaming-failure-unknown-model.txt", HttpStatusCode.NotFound) { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -204,7 +206,7 @@ internal class StreamingSnapshotTests { @Test fun `invalid api key`() = - goldenStreamingFile("streaming-failure-api-key.txt", HttpStatusCode.BadRequest) { + goldenVertexStreamingFile("streaming-failure-api-key.txt", HttpStatusCode.BadRequest) { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -212,7 +214,7 @@ internal class StreamingSnapshotTests { @Test fun `invalid json`() = - goldenStreamingFile("streaming-failure-invalid-json.txt") { + goldenVertexStreamingFile("streaming-failure-invalid-json.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } @@ -220,7 +222,7 @@ internal class StreamingSnapshotTests { @Test fun `malformed content`() = - goldenStreamingFile("streaming-failure-malformed-content.txt") { + goldenVertexStreamingFile("streaming-failure-malformed-content.txt") { val responses = model.generateContentStream("prompt") withTimeout(testTimeout) { shouldThrow { responses.collect() } } diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIUnarySnapshotTests.kt similarity index 84% rename from firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt rename to firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIUnarySnapshotTests.kt index a7ed1c4ed7f..f3603814423 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/UnarySnapshotTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/VertexAIUnarySnapshotTests.kt @@ -34,7 +34,7 @@ import com.google.firebase.vertexai.type.ServerException import com.google.firebase.vertexai.type.ServiceDisabledException import com.google.firebase.vertexai.type.TextPart import com.google.firebase.vertexai.type.UnsupportedUserLocationException -import com.google.firebase.vertexai.util.goldenUnaryFile +import com.google.firebase.vertexai.util.goldenVertexUnaryFile import com.google.firebase.vertexai.util.shouldNotBeNullOrEmpty import io.kotest.assertions.throwables.shouldThrow import io.kotest.inspectors.forAtLeastOne @@ -57,12 +57,12 @@ import org.json.JSONArray import org.junit.Test @OptIn(PublicPreviewAPI::class) -internal class UnarySnapshotTests { +internal class VertexAIUnarySnapshotTests { private val testTimeout = 5.seconds @Test fun `short reply`() = - goldenUnaryFile("unary-success-basic-reply-short.json") { + goldenVertexUnaryFile("unary-success-basic-reply-short.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -75,7 +75,7 @@ internal class UnarySnapshotTests { @Test fun `long reply`() = - goldenUnaryFile("unary-success-basic-reply-long.json") { + goldenVertexUnaryFile("unary-success-basic-reply-long.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -88,7 +88,7 @@ internal class UnarySnapshotTests { @Test fun `response with detailed token-based usageMetadata`() = - goldenUnaryFile("unary-success-basic-response-long-usage-metadata.json") { + goldenVertexUnaryFile("unary-success-basic-response-long-usage-metadata.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -113,7 +113,7 @@ internal class UnarySnapshotTests { @Test fun `unknown enum in safety ratings`() = - goldenUnaryFile("unary-success-unknown-enum-safety-ratings.json") { + goldenVertexUnaryFile("unary-success-unknown-enum-safety-ratings.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -127,7 +127,7 @@ internal class UnarySnapshotTests { @Test fun `unknown enum in finish reason`() = - goldenUnaryFile("unary-failure-unknown-enum-finish-reason.json") { + goldenVertexUnaryFile("unary-failure-unknown-enum-finish-reason.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } should { @@ -138,7 +138,7 @@ internal class UnarySnapshotTests { @Test fun `unknown enum in block reason`() = - goldenUnaryFile("unary-failure-unknown-enum-prompt-blocked.json") { + goldenVertexUnaryFile("unary-failure-unknown-enum-prompt-blocked.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } should { @@ -149,7 +149,7 @@ internal class UnarySnapshotTests { @Test fun `quotes escaped`() = - goldenUnaryFile("unary-success-quote-reply.json") { + goldenVertexUnaryFile("unary-success-quote-reply.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -162,7 +162,7 @@ internal class UnarySnapshotTests { @Test fun `safetyRatings missing`() = - goldenUnaryFile("unary-success-missing-safety-ratings.json") { + goldenVertexUnaryFile("unary-success-missing-safety-ratings.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -175,7 +175,7 @@ internal class UnarySnapshotTests { @Test fun `safetyRatings including severity`() = - goldenUnaryFile("unary-success-including-severity.json") { + goldenVertexUnaryFile("unary-success-including-severity.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -193,7 +193,7 @@ internal class UnarySnapshotTests { @Test fun `function call has no arguments field`() = - goldenUnaryFile("unary-success-function-call-empty-arguments.json") { + goldenVertexUnaryFile("unary-success-function-call-empty-arguments.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val content = response.candidates.shouldNotBeNullOrEmpty().first().content @@ -207,7 +207,7 @@ internal class UnarySnapshotTests { @Test fun `prompt blocked for safety`() = - goldenUnaryFile("unary-failure-prompt-blocked-safety.json") { + goldenVertexUnaryFile("unary-failure-prompt-blocked-safety.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } should { @@ -218,7 +218,7 @@ internal class UnarySnapshotTests { @Test fun `prompt blocked for safety with message`() = - goldenUnaryFile("unary-failure-prompt-blocked-safety-with-message.json") { + goldenVertexUnaryFile("unary-failure-prompt-blocked-safety-with-message.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } should { @@ -230,7 +230,7 @@ internal class UnarySnapshotTests { @Test fun `empty content`() = - goldenUnaryFile("unary-failure-empty-content.json") { + goldenVertexUnaryFile("unary-failure-empty-content.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -238,13 +238,13 @@ internal class UnarySnapshotTests { @Test fun `http error`() = - goldenUnaryFile("unary-failure-http-error.json", HttpStatusCode.PreconditionFailed) { + goldenVertexUnaryFile("unary-failure-http-error.json", HttpStatusCode.PreconditionFailed) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } } @Test fun `user location error`() = - goldenUnaryFile( + goldenVertexUnaryFile( "unary-failure-unsupported-user-location.json", HttpStatusCode.PreconditionFailed, ) { @@ -255,7 +255,7 @@ internal class UnarySnapshotTests { @Test fun `stopped for safety`() = - goldenUnaryFile("unary-failure-finish-reason-safety.json") { + goldenVertexUnaryFile("unary-failure-finish-reason-safety.json") { withTimeout(testTimeout) { val exception = shouldThrow { model.generateContent("prompt") } exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY @@ -269,7 +269,7 @@ internal class UnarySnapshotTests { @Test fun `quota exceeded`() = - goldenUnaryFile("unary-failure-quota-exceeded.json", HttpStatusCode.BadRequest) { + goldenVertexUnaryFile("unary-failure-quota-exceeded.json", HttpStatusCode.BadRequest) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -277,7 +277,7 @@ internal class UnarySnapshotTests { @Test fun `stopped for safety with no content`() = - goldenUnaryFile("unary-failure-finish-reason-safety-no-content.json") { + goldenVertexUnaryFile("unary-failure-finish-reason-safety-no-content.json") { withTimeout(testTimeout) { val exception = shouldThrow { model.generateContent("prompt") } exception.response.candidates.first().finishReason shouldBe FinishReason.SAFETY @@ -286,7 +286,7 @@ internal class UnarySnapshotTests { @Test fun `citation returns correctly`() = - goldenUnaryFile("unary-success-citations.json") { + goldenVertexUnaryFile("unary-success-citations.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -301,7 +301,7 @@ internal class UnarySnapshotTests { @Test fun `citation returns correctly with missing license and startIndex`() = - goldenUnaryFile("unary-success-citations-nolicense.json") { + goldenVertexUnaryFile("unary-success-citations-nolicense.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -320,7 +320,7 @@ internal class UnarySnapshotTests { @Test fun `response includes usage metadata`() = - goldenUnaryFile("unary-success-usage-metadata.json") { + goldenVertexUnaryFile("unary-success-usage-metadata.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -334,7 +334,7 @@ internal class UnarySnapshotTests { @Test fun `response includes partial usage metadata`() = - goldenUnaryFile("unary-success-partial-usage-metadata.json") { + goldenVertexUnaryFile("unary-success-partial-usage-metadata.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") @@ -348,7 +348,7 @@ internal class UnarySnapshotTests { @Test fun `properly translates json text`() = - goldenUnaryFile("unary-success-constraint-decoding-json.json") { + goldenVertexUnaryFile("unary-success-constraint-decoding-json.json") { val response = model.generateContent("prompt") response.candidates.isEmpty() shouldBe false @@ -368,7 +368,7 @@ internal class UnarySnapshotTests { @Test fun `invalid response`() = - goldenUnaryFile("unary-failure-invalid-response.json") { + goldenVertexUnaryFile("unary-failure-invalid-response.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -376,7 +376,7 @@ internal class UnarySnapshotTests { @Test fun `malformed content`() = - goldenUnaryFile("unary-failure-malformed-content.json") { + goldenVertexUnaryFile("unary-failure-malformed-content.json") { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -384,7 +384,7 @@ internal class UnarySnapshotTests { @Test fun `invalid api key`() = - goldenUnaryFile("unary-failure-api-key.json", HttpStatusCode.BadRequest) { + goldenVertexUnaryFile("unary-failure-api-key.json", HttpStatusCode.BadRequest) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -392,19 +392,22 @@ internal class UnarySnapshotTests { @Test fun `image rejected`() = - goldenUnaryFile("unary-failure-image-rejected.json", HttpStatusCode.BadRequest) { + goldenVertexUnaryFile("unary-failure-image-rejected.json", HttpStatusCode.BadRequest) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } } @Test fun `unknown model`() = - goldenUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) { + goldenVertexUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } } @Test fun `service disabled`() = - goldenUnaryFile("unary-failure-firebaseml-api-not-enabled.json", HttpStatusCode.Forbidden) { + goldenVertexUnaryFile( + "unary-failure-firebaseml-api-not-enabled.json", + HttpStatusCode.Forbidden + ) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } @@ -412,7 +415,7 @@ internal class UnarySnapshotTests { @Test fun `function call contains null param`() = - goldenUnaryFile("unary-success-function-call-null.json") { + goldenVertexUnaryFile("unary-success-function-call-null.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val callPart = (response.candidates.first().content.parts.first() as FunctionCallPart) @@ -423,7 +426,7 @@ internal class UnarySnapshotTests { @Test fun `function call contains json literal`() = - goldenUnaryFile("unary-success-function-call-json-literal.json") { + goldenVertexUnaryFile("unary-success-function-call-json-literal.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val content = response.candidates.shouldNotBeNullOrEmpty().first().content @@ -440,7 +443,7 @@ internal class UnarySnapshotTests { @Test fun `function call with complex json literal parses correctly`() = - goldenUnaryFile("unary-success-function-call-complex-json-literal.json") { + goldenVertexUnaryFile("unary-success-function-call-complex-json-literal.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val content = response.candidates.shouldNotBeNullOrEmpty().first().content @@ -459,7 +462,7 @@ internal class UnarySnapshotTests { @Test fun `function call contains no arguments`() = - goldenUnaryFile("unary-success-function-call-no-arguments.json") { + goldenVertexUnaryFile("unary-success-function-call-no-arguments.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val callPart = response.functionCalls.shouldNotBeEmpty().first() @@ -471,7 +474,7 @@ internal class UnarySnapshotTests { @Test fun `function call contains arguments`() = - goldenUnaryFile("unary-success-function-call-with-arguments.json") { + goldenVertexUnaryFile("unary-success-function-call-with-arguments.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val callPart = response.functionCalls.shouldNotBeEmpty().first() @@ -484,7 +487,7 @@ internal class UnarySnapshotTests { @Test fun `function call with parallel calls`() = - goldenUnaryFile("unary-success-function-call-parallel-calls.json") { + goldenVertexUnaryFile("unary-success-function-call-parallel-calls.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val callList = response.functionCalls @@ -499,7 +502,7 @@ internal class UnarySnapshotTests { @Test fun `function call with mixed content`() = - goldenUnaryFile("unary-success-function-call-mixed-content.json") { + goldenVertexUnaryFile("unary-success-function-call-mixed-content.json") { withTimeout(testTimeout) { val response = model.generateContent("prompt") val callList = response.functionCalls @@ -512,7 +515,7 @@ internal class UnarySnapshotTests { @Test fun `countTokens succeeds`() = - goldenUnaryFile("unary-success-total-tokens.json") { + goldenVertexUnaryFile("unary-success-total-tokens.json") { withTimeout(testTimeout) { val response = model.countTokens("prompt") @@ -524,7 +527,7 @@ internal class UnarySnapshotTests { @Test fun `countTokens with modality fields returned`() = - goldenUnaryFile("unary-success-detailed-token-response.json") { + goldenVertexUnaryFile("unary-success-detailed-token-response.json") { withTimeout(testTimeout) { val response = model.countTokens("prompt") @@ -540,7 +543,7 @@ internal class UnarySnapshotTests { @Test fun `countTokens succeeds with no billable characters`() = - goldenUnaryFile("unary-success-no-billable-characters.json") { + goldenVertexUnaryFile("unary-success-no-billable-characters.json") { withTimeout(testTimeout) { val response = model.countTokens("prompt") @@ -551,13 +554,13 @@ internal class UnarySnapshotTests { @Test fun `countTokens fails with model not found`() = - goldenUnaryFile("unary-failure-model-not-found.json", HttpStatusCode.NotFound) { + goldenVertexUnaryFile("unary-failure-model-not-found.json", HttpStatusCode.NotFound) { withTimeout(testTimeout) { shouldThrow { model.countTokens("prompt") } } } @Test fun `generateImages should throw when all images filtered`() = - goldenUnaryFile("unary-failure-generate-images-all-filtered.json") { + goldenVertexUnaryFile("unary-failure-generate-images-all-filtered.json") { withTimeout(testTimeout) { shouldThrow { imagenModel.generateImages("prompt") } } @@ -565,7 +568,7 @@ internal class UnarySnapshotTests { @Test fun `generateImages should throw when prompt blocked`() = - goldenUnaryFile( + goldenVertexUnaryFile( "unary-failure-generate-images-prompt-blocked.json", HttpStatusCode.BadRequest, ) { diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt index 9428aea67ef..4f648735396 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt @@ -125,7 +125,7 @@ internal fun commonTest( * @param name The name of the *Golden File* to load * @param httpStatusCode An optional [HttpStatusCode] to return as a response * @param block The test contents themselves, with a [CommonTestScope] implicitly provided - * @see goldenUnaryFile + * @see goldenVertexUnaryFile */ internal fun goldenStreamingFile( name: String, @@ -147,6 +147,23 @@ internal fun goldenStreamingFile( } } +/** + * A variant of [goldenStreamingFile] for testing vertexAI + * + * Loads the *Golden File* and automatically parses the messages from it; providing it to the + * channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenStreamingFile + */ +internal fun goldenVertexStreamingFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenStreamingFile("vertexai/$name", httpStatusCode, block) + /** * A variant of [commonTest] for performing snapshot tests. * @@ -171,6 +188,21 @@ internal fun goldenUnaryFile( block() } +/** + * A variant of [goldenUnaryFile] for vertexai tests Loads the *Golden File* and automatically + * provides it to the channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenUnaryFile + */ +internal fun goldenVertexUnaryFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenUnaryFile("vertexai/$name", httpStatusCode, block) + /** * Loads a *Golden File* from the resource directory. * diff --git a/firebase-vertexai/update_responses.sh b/firebase-vertexai/update_responses.sh index 3feec3b861b..28b0be1a25a 100755 --- a/firebase-vertexai/update_responses.sh +++ b/firebase-vertexai/update_responses.sh @@ -17,7 +17,7 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v6.*' # The major version of mock responses to use +RESPONSES_VERSION='v7.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" From 76990d93f131a392a0d07e90948985506af826cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:51:51 -0400 Subject: [PATCH 048/162] build(deps): bump androidx.core:core from 1.2.0 to 1.15.0 (#6764) Bumps androidx.core:core from 1.2.0 to 1.15.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=androidx.core:core&package-manager=gradle&previous-version=1.2.0&new-version=1.15.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Rodrigo Lazo Paz --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c191d08d81..4db41dbe493 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,7 +90,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" } androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } -androidx-core = { module = "androidx.core:core", version = "1.2.0" } +androidx-core = { module = "androidx.core:core", version = "1.13.1" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } From f86d40df2a678f808267fb499838a1deb0f7070b Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Wed, 19 Mar 2025 07:24:16 -0600 Subject: [PATCH 049/162] Simplify settings package and a few more classes using DI (#6778) Simplify settings package and a few more classes using DI. The biggest example is `SessionsSettings`, which used to have multiple secondary constructors. This change lets us avoid plumbing dependencies like `DataStore<>` through multiple classes. Moved DataStore construction into provides, which will be needed for `MultiProcessDataStoreFactory`. Also cleaned up tests a bit. We shouldn't use Dagger in the unit tests on Android. Once we remove the bound service, it will be easy to remove DI from the unit tests. Instead, unit tests will just call the constructors directly and pass in real or fake instances. --- .../sessions/FirebaseSessionsComponent.kt | 81 +++++++++++++++- .../firebase/sessions/SessionDatastore.kt | 20 +--- .../firebase/sessions/SessionGenerator.kt | 11 +-- .../google/firebase/sessions/TimeProvider.kt | 5 +- .../google/firebase/sessions/UuidGenerator.kt | 29 ++++++ .../settings/LocalOverrideSettings.kt | 15 ++- .../sessions/settings/RemoteSettings.kt | 19 ++-- .../settings/RemoteSettingsFetcher.kt | 17 ++-- .../sessions/settings/SessionsSettings.kt | 73 ++------------- .../sessions/settings/SettingsCache.kt | 15 +-- .../firebase/sessions/SessionGeneratorTest.kt | 41 ++------ .../sessions/SessionLifecycleClientTest.kt | 25 ++--- .../sessions/SessionLifecycleServiceTest.kt | 40 +++----- .../SessionsActivityLifecycleCallbacksTest.kt | 25 ++--- .../sessions/settings/RemoteSettingsTest.kt | 93 ++++++++++--------- .../sessions/settings/SessionsSettingsTest.kt | 21 +++-- .../sessions/testing/FakeUuidGenerator.kt | 37 ++++++++ .../testing/FirebaseSessionsFakeComponent.kt | 44 ++++++--- .../testing/FirebaseSessionsFakeRegistrar.kt | 75 ++------------- 19 files changed, 354 insertions(+), 332 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index aa60f3f41df..5680c9cc0ec 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -17,22 +17,48 @@ package com.google.firebase.sessions import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStoreFile import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.inject.Provider import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName +import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher +import com.google.firebase.sessions.settings.LocalOverrideSettings +import com.google.firebase.sessions.settings.RemoteSettings +import com.google.firebase.sessions.settings.RemoteSettingsFetcher import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider import dagger.Binds import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Qualifier import javax.inject.Singleton import kotlin.coroutines.CoroutineContext -/** Dagger component to provide [FirebaseSessions] and its dependencies. */ +@Qualifier internal annotation class SessionConfigsDataStore + +@Qualifier internal annotation class SessionDetailsDataStore + +@Qualifier internal annotation class LocalOverrideSettingsProvider + +@Qualifier internal annotation class RemoteSettingsProvider + +/** + * Dagger component to provide [FirebaseSessions] and its dependencies. + * + * This gets configured and built in [FirebaseSessionsRegistrar.getComponents]. + */ @Singleton @Component(modules = [FirebaseSessionsComponent.MainModule::class]) internal interface FirebaseSessionsComponent { @@ -79,8 +105,59 @@ internal interface FirebaseSessionsComponent { impl: SessionLifecycleServiceBinderImpl ): SessionLifecycleServiceBinder + @Binds + @Singleton + fun crashlyticsSettingsFetcher(impl: RemoteSettingsFetcher): CrashlyticsSettingsFetcher + + @Binds + @Singleton + @LocalOverrideSettingsProvider + fun localOverrideSettings(impl: LocalOverrideSettings): SettingsProvider + + @Binds + @Singleton + @RemoteSettingsProvider + fun remoteSettings(impl: RemoteSettings): SettingsProvider + companion object { - @Provides @Singleton fun sessionGenerator() = SessionGenerator(timeProvider = WallClock) + private const val TAG = "FirebaseSessions" + + @Provides @Singleton fun timeProvider(): TimeProvider = TimeProviderImpl + + @Provides @Singleton fun uuidGenerator(): UuidGenerator = UuidGeneratorImpl + + @Provides + @Singleton + fun applicationInfo(firebaseApp: FirebaseApp): ApplicationInfo = + SessionEvents.getApplicationInfo(firebaseApp) + + @Provides + @Singleton + @SessionConfigsDataStore + fun sessionConfigsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME) + } + + @Provides + @Singleton + @SessionDetailsDataStore + fun sessionDetailsDataStore(appContext: Context): DataStore = + PreferenceDataStoreFactory.create( + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) + emptyPreferences() + } + ) { + appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME) + } } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index a2d46a48891..2c4f243f942 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -16,19 +16,15 @@ package com.google.firebase.sessions -import android.content.Context import android.util.Log import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import java.io.IOException import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -64,8 +60,8 @@ internal interface SessionDatastore { internal class SessionDatastoreImpl @Inject constructor( - private val appContext: Context, @Background private val backgroundDispatcher: CoroutineContext, + @SessionDetailsDataStore private val dataStore: DataStore, ) : SessionDatastore { /** Most recent session from datastore is updated asynchronously whenever it changes */ @@ -76,7 +72,7 @@ constructor( } private val firebaseSessionDataFlow: Flow = - appContext.dataStore.data + dataStore.data .catch { exception -> Log.e(TAG, "Error reading stored session data.", exception) emit(emptyPreferences()) @@ -92,7 +88,7 @@ constructor( override fun updateSessionId(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { try { - appContext.dataStore.edit { preferences -> + dataStore.edit { preferences -> preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId } } catch (e: IOException) { @@ -108,15 +104,5 @@ constructor( private companion object { private const val TAG = "FirebaseSessionsRepo" - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SESSIONS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 41aeb442cfb..4c4775e8b24 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -19,7 +19,7 @@ package com.google.firebase.sessions import com.google.errorprone.annotations.CanIgnoreReturnValue import com.google.firebase.Firebase import com.google.firebase.app -import java.util.UUID +import javax.inject.Inject import javax.inject.Singleton /** @@ -37,10 +37,9 @@ internal data class SessionDetails( * [SessionDetails] up to date with the latest values. */ @Singleton -internal class SessionGenerator( - private val timeProvider: TimeProvider, - private val uuidGenerator: () -> UUID = UUID::randomUUID, -) { +internal class SessionGenerator +@Inject +constructor(private val timeProvider: TimeProvider, private val uuidGenerator: UuidGenerator) { private val firstSessionId = generateSessionId() private var sessionIndex = -1 @@ -66,7 +65,7 @@ internal class SessionGenerator( return currentSession } - private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase() + private fun generateSessionId() = uuidGenerator.next().toString().replace("-", "").lowercase() internal companion object { val instance: SessionGenerator diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index 706285de337..b66b09af19f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -23,11 +23,12 @@ import kotlin.time.Duration.Companion.milliseconds /** Time provider interface, for testing purposes. */ internal interface TimeProvider { fun elapsedRealtime(): Duration + fun currentTimeUs(): Long } -/** "Wall clock" time provider. */ -internal object WallClock : TimeProvider { +/** "Wall clock" time provider implementation. */ +internal object TimeProviderImpl : TimeProvider { /** * Gets the [Duration] elapsed in "wall clock" time since device boot. * diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt new file mode 100644 index 00000000000..8c5b153fef2 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/UuidGenerator.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions + +import java.util.UUID + +/** UUID generator interface. */ +internal fun interface UuidGenerator { + fun next(): UUID +} + +/** Generate random UUIDs using [UUID.randomUUID]. */ +internal object UuidGeneratorImpl : UuidGenerator { + override fun next(): UUID = UUID.randomUUID() +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt index 37e7acc949b..f13d0ffde2e 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/LocalOverrideSettings.kt @@ -19,20 +19,19 @@ package com.google.firebase.sessions.settings import android.content.Context import android.content.pm.PackageManager import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration -internal class LocalOverrideSettings(context: Context) : SettingsProvider { - @Suppress("DEPRECATION") // TODO(mrober): Use ApplicationInfoFlags when target sdk set to 33 +@Singleton +internal class LocalOverrideSettings @Inject constructor(appContext: Context) : SettingsProvider { private val metadata = - context.packageManager - .getApplicationInfo( - context.packageName, - PackageManager.GET_META_DATA, - ) + appContext.packageManager + .getApplicationInfo(appContext.packageName, PackageManager.GET_META_DATA) .metaData - ?: Bundle.EMPTY // Default to an empty bundle, meaning no cached values. + ?: Bundle.EMPTY // Default to an empty bundle override val sessionEnabled: Boolean? get() = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt index 1e6015a5c0d..67a48bc7924 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt @@ -19,11 +19,13 @@ package com.google.firebase.sessions.settings import android.os.Build import android.util.Log import androidx.annotation.VisibleForTesting -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.InstallationId +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -34,14 +36,19 @@ import kotlinx.coroutines.sync.withLock import org.json.JSONException import org.json.JSONObject -internal class RemoteSettings( - private val backgroundDispatcher: CoroutineContext, +@Singleton +internal class RemoteSettings +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineContext, private val firebaseInstallationsApi: FirebaseInstallationsApi, private val appInfo: ApplicationInfo, private val configsFetcher: CrashlyticsSettingsFetcher, - dataStore: DataStore, + private val lazySettingsCache: Lazy, ) : SettingsProvider { - private val settingsCache by lazy { SettingsCache(dataStore) } + private val settingsCache: SettingsCache + get() = lazySettingsCache.get() + private val fetchInProgress = Mutex() override val sessionEnabled: Boolean? diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt index a0896c24e7e..92d530f2fa1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt @@ -17,10 +17,13 @@ package com.google.firebase.sessions.settings import android.net.Uri +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.sessions.ApplicationInfo import java.io.BufferedReader import java.io.InputStreamReader import java.net.URL +import javax.inject.Inject +import javax.inject.Singleton import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @@ -30,20 +33,22 @@ internal fun interface CrashlyticsSettingsFetcher { suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (msg: String) -> Unit + onFailure: suspend (msg: String) -> Unit, ) } -internal class RemoteSettingsFetcher( +@Singleton +internal class RemoteSettingsFetcher +@Inject +constructor( private val appInfo: ApplicationInfo, - private val blockingDispatcher: CoroutineContext, - private val baseUrl: String = FIREBASE_SESSIONS_BASE_URL_STRING, + @Background private val blockingDispatcher: CoroutineContext, ) : CrashlyticsSettingsFetcher { @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls. override suspend fun doConfigFetch( headerOptions: Map, onSuccess: suspend (JSONObject) -> Unit, - onFailure: suspend (String) -> Unit + onFailure: suspend (String) -> Unit, ) = withContext(blockingDispatcher) { try { @@ -78,7 +83,7 @@ internal class RemoteSettingsFetcher( val uri = Uri.Builder() .scheme("https") - .authority(baseUrl) + .authority(FIREBASE_SESSIONS_BASE_URL_STRING) .appendPath("spi") .appendPath("v2") .appendPath("platforms") diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index 41b73f14a4e..d319bebb7a2 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -16,71 +16,24 @@ package com.google.firebase.sessions.settings -import android.content.Context -import android.util.Log -import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStore import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.app -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.FirebaseSessionsComponent -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName -import com.google.firebase.sessions.SessionDataStoreConfigs -import com.google.firebase.sessions.SessionEvents +import com.google.firebase.sessions.LocalOverrideSettingsProvider +import com.google.firebase.sessions.RemoteSettingsProvider import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes /** [SessionsSettings] manages all the configs that are relevant to the sessions library. */ @Singleton -internal class SessionsSettings( - private val localOverrideSettings: SettingsProvider, - private val remoteSettings: SettingsProvider, +internal class SessionsSettings +@Inject +constructor( + @LocalOverrideSettingsProvider private val localOverrideSettings: SettingsProvider, + @RemoteSettingsProvider private val remoteSettings: SettingsProvider, ) { - private constructor( - context: Context, - blockingDispatcher: CoroutineContext, - backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - appInfo: ApplicationInfo, - ) : this( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = - RemoteSettings( - backgroundDispatcher, - firebaseInstallationsApi, - appInfo, - configsFetcher = - RemoteSettingsFetcher( - appInfo, - blockingDispatcher, - ), - dataStore = context.dataStore, - ), - ) - - @Inject - constructor( - firebaseApp: FirebaseApp, - @Blocking blockingDispatcher: CoroutineContext, - @Background backgroundDispatcher: CoroutineContext, - firebaseInstallationsApi: FirebaseInstallationsApi, - ) : this( - firebaseApp.applicationContext, - blockingDispatcher, - backgroundDispatcher, - firebaseInstallationsApi, - SessionEvents.getApplicationInfo(firebaseApp), - ) // Order of preference for all the configs below: // 1. Honor local overrides @@ -147,19 +100,7 @@ internal class SessionsSettings( } internal companion object { - private const val TAG = "SessionsSettings" - val instance: SessionsSettings get() = Firebase.app[FirebaseSessionsComponent::class.java].sessionsSettings - - private val Context.dataStore: DataStore by - preferencesDataStore( - name = SessionDataStoreConfigs.SETTINGS_CONFIG_NAME, - corruptionHandler = - ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) - emptyPreferences() - }, - ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt index 33b6a4fe7c8..2e60e51650a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt @@ -25,7 +25,10 @@ import androidx.datastore.preferences.core.doublePreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey +import com.google.firebase.sessions.SessionConfigsDataStore import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -37,7 +40,10 @@ internal data class SessionConfigs( val cacheUpdatedTime: Long?, ) -internal class SettingsCache(private val dataStore: DataStore) { +@Singleton +internal class SettingsCache +@Inject +constructor(@SessionConfigsDataStore private val dataStore: DataStore) { private lateinit var sessionConfigs: SessionConfigs init { @@ -54,7 +60,7 @@ internal class SettingsCache(private val dataStore: DataStore) { sessionSamplingRate = preferences[SAMPLING_RATE], sessionRestartTimeout = preferences[RESTART_TIMEOUT_SECONDS], cacheDuration = preferences[CACHE_DURATION_SECONDS], - cacheUpdatedTime = preferences[CACHE_UPDATED_TIME] + cacheUpdatedTime = preferences[CACHE_UPDATED_TIME], ) } @@ -105,10 +111,7 @@ internal class SettingsCache(private val dataStore: DataStore) { updateSessionConfigs(preferences) } } catch (e: IOException) { - Log.w( - TAG, - "Failed to remove config values: $e", - ) + Log.w(TAG, "Failed to remove config values: $e") } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index 7f29fb66ae7..7126bae4dbf 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -18,8 +18,8 @@ package com.google.firebase.sessions import com.google.common.truth.Truth.assertThat import com.google.firebase.sessions.testing.FakeTimeProvider +import com.google.firebase.sessions.testing.FakeUuidGenerator import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US -import java.util.UUID import org.junit.Test class SessionGeneratorTest { @@ -41,9 +41,7 @@ class SessionGeneratorTest { @Test(expected = UninitializedPropertyAccessException::class) fun currentSession_beforeGenerate_throwsUninitialized() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.currentSession } @@ -51,9 +49,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_beforeGenerate_returnsFalse() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) assertThat(sessionGenerator.hasGenerateSession).isFalse() } @@ -61,9 +57,7 @@ class SessionGeneratorTest { @Test fun hasGenerateSession_afterGenerate_returnsTrue() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -73,9 +67,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionIds() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = UuidGeneratorImpl) sessionGenerator.generateNewSession() @@ -91,10 +83,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_generatesValidSessionDetails() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) sessionGenerator.generateNewSession() @@ -117,10 +106,7 @@ class SessionGeneratorTest { @Test fun generateNewSession_incrementsSessionIndex_keepsFirstSessionId() { val sessionGenerator = - SessionGenerator( - timeProvider = FakeTimeProvider(), - uuidGenerator = UUIDs()::next, - ) + SessionGenerator(timeProvider = FakeTimeProvider(), uuidGenerator = FakeUuidGenerator()) val firstSessionDetails = sessionGenerator.generateNewSession() @@ -170,22 +156,9 @@ class SessionGeneratorTest { ) } - private class UUIDs(val names: List = listOf(UUID_1, UUID_2, UUID_3)) { - var index = -1 - - fun next(): UUID { - index = (index + 1).coerceAtMost(names.size - 1) - return UUID.fromString(names[index]) - } - } - - @Suppress("SpellCheckingInspection") // UUIDs are not words. companion object { - const val UUID_1 = "11111111-1111-1111-1111-111111111111" const val SESSION_ID_1 = "11111111111111111111111111111111" - const val UUID_2 = "22222222-2222-2222-2222-222222222222" const val SESSION_ID_2 = "22222222222222222222222222222222" - const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" const val SESSION_ID_3 = "cccccccccccccccccccccccccccccccc" } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt index b038e68081c..12a017a7462 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -47,21 +48,21 @@ import org.robolectric.Shadows.shadowOf @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleClientTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder @Before fun setUp() { - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt index 682a9ddfbbb..ccd933f1213 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt @@ -16,7 +16,6 @@ package com.google.firebase.sessions -import android.content.Context import android.content.Intent import android.os.Handler import android.os.Looper @@ -30,10 +29,8 @@ import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.initialize import com.google.firebase.sessions.testing.FakeFirebaseApp -import com.google.firebase.sessions.testing.FakeFirelogPublisher -import com.google.firebase.sessions.testing.FakeSessionDatastore +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import java.time.Duration -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Before import org.junit.Test @@ -46,14 +43,11 @@ import org.robolectric.annotation.LooperMode import org.robolectric.annotation.LooperMode.Mode.PAUSED import org.robolectric.shadows.ShadowSystemClock -@OptIn(ExperimentalCoroutinesApi::class) @MediumTest @LooperMode(PAUSED) @RunWith(RobolectricTestRunner::class) internal class SessionLifecycleServiceTest { - - lateinit var service: ServiceController - lateinit var firebaseApp: FirebaseApp + private lateinit var service: ServiceController data class CallbackMessage(val code: Int, val sessionId: String?) @@ -68,16 +62,14 @@ internal class SessionLifecycleServiceTest { @Before fun setUp() { - val context = ApplicationProvider.getApplicationContext() - firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build() - ) + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) service = createService() } @@ -99,7 +91,7 @@ internal class SessionLifecycleServiceTest { @Test fun binding_callbackOnInitialBindWhenSessionIdSet() { val client = TestCallbackHandler() - firebaseApp.get(FakeSessionDatastore::class.java).updateSessionId("123") + FirebaseSessionsFakeComponent.instance.fakeSessionDatastore.updateSessionId("123") bindToService(client) @@ -222,11 +214,9 @@ internal class SessionLifecycleServiceTest { } private fun createServiceLaunchIntent(client: TestCallbackHandler) = - Intent( - ApplicationProvider.getApplicationContext(), - SessionLifecycleService::class.java - ) - .apply { putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) } + Intent(ApplicationProvider.getApplicationContext(), SessionLifecycleService::class.java).apply { + putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) + } private fun createService() = Robolectric.buildService(SessionLifecycleService::class.java).create() @@ -237,7 +227,7 @@ internal class SessionLifecycleServiceTest { } private fun getUploadedSessions() = - firebaseApp.get(FakeFirelogPublisher::class.java).loggedSessions + FirebaseSessionsFakeComponent.instance.fakeFirelogPublisher.loggedSessions private fun getSessionId(msg: Message) = msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt index 189e13fed89..62e650d90c8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt @@ -31,6 +31,7 @@ import com.google.firebase.sessions.api.SessionSubscriber import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder import com.google.firebase.sessions.testing.FakeSessionSubscriber +import com.google.firebase.sessions.testing.FirebaseSessionsFakeComponent import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher @@ -46,7 +47,7 @@ import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) internal class SessionsActivityLifecycleCallbacksTest { private lateinit var fakeService: FakeSessionLifecycleServiceBinder - private lateinit var lifecycleServiceBinder: FakeSessionLifecycleServiceBinder + private lateinit var lifecycleServiceBinder: SessionLifecycleServiceBinder private val fakeActivity = Activity() @Before @@ -63,17 +64,17 @@ internal class SessionsActivityLifecycleCallbacksTest { ) ) - val firebaseApp = - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) - .setApiKey(FakeFirebaseApp.MOCK_API_KEY) - .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) - .build(), - ) - fakeService = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] - lifecycleServiceBinder = firebaseApp[FakeSessionLifecycleServiceBinder::class.java] + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build(), + ) + + fakeService = FirebaseSessionsFakeComponent.instance.fakeSessionLifecycleServiceBinder + lifecycleServiceBinder = FirebaseSessionsFakeComponent.instance.sessionLifecycleServiceBinder } @After diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index 6a3a4a1f8c3..e4fb0b00148 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -22,11 +22,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.SessionEvents import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher -import com.google.firebase.sessions.testing.TestSessionEventData.TEST_APPLICATION_INFO +import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers @@ -55,19 +57,18 @@ class RemoteSettingsTest { val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcher = FakeRemoteConfigFetcher() - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext - val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) runCurrent() @@ -97,16 +98,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) runCurrent() @@ -138,16 +140,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -190,16 +193,17 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -248,26 +252,24 @@ class RemoteSettingsTest { val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcherWithDelay = - FakeRemoteConfigFetcher( - JSONObject(VALID_RESPONSE), - networkDelay = 3.seconds, - ) + FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) fakeFetcherWithDelay.responseJSONObject .getJSONObject("app_quality") .put("sampling_rate", 0.125) val remoteSettingsWithDelay = - RemoteSettings( + buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), - configsFetcher = fakeFetcherWithDelay, - dataStore = + fakeFetcherWithDelay, + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) // Do the first fetch. This one should fetched the configsFetcher. @@ -290,30 +292,12 @@ class RemoteSettingsTest { assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125) } - @Test - fun remoteSettingsFetcher_badFetch_callsOnFailure() = runTest { - var failure: String? = null - - RemoteSettingsFetcher( - TEST_APPLICATION_INFO, - TestOnlyExecutors.blocking().asCoroutineDispatcher() + coroutineContext, - baseUrl = "this.url.is.invalid", - ) - .doConfigFetch( - headerOptions = emptyMap(), - onSuccess = {}, - onFailure = { failure = it }, - ) - - assertThat(failure).isNotNull() - } - @After fun cleanUp() { FirebaseApp.clearInstancesForTest() } - private companion object { + internal companion object { const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test" const val VALID_RESPONSE = @@ -334,5 +318,30 @@ class RemoteSettingsTest { } } """ + + /** + * Build an instance of [RemoteSettings] using the Dagger factory. + * + * This is needed because the SDK vendors Dagger to a difference namespace, but it does not for + * these unit tests. The [RemoteSettings.lazySettingsCache] has type [dagger.Lazy] in these + * tests, but type `com.google.firebase.sessions.dagger.Lazy` in the SDK. This method to build + * the instance is the easiest I could find that does not need any reference to [dagger.Lazy] in + * the test code. + */ + fun buildRemoteSettings( + backgroundDispatcher: CoroutineContext, + firebaseInstallationsApi: FirebaseInstallationsApi, + appInfo: ApplicationInfo, + configsFetcher: CrashlyticsSettingsFetcher, + settingsCache: SettingsCache, + ): RemoteSettings = + RemoteSettings_Factory.create( + { backgroundDispatcher }, + { firebaseInstallationsApi }, + { appInfo }, + { configsFetcher }, + { settingsCache }, + ) + .get() } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index f74eac409e5..12f40e7cca8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -107,16 +107,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = @@ -149,16 +150,17 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = @@ -197,16 +199,17 @@ class SessionsSettingsTest { fakeFetcher.responseJSONObject = JSONObject(invalidResponse) val remoteSettings = - RemoteSettings( + RemoteSettingsTest.buildRemoteSettings( TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - dataStore = + SettingsCache( PreferenceDataStoreFactory.create( scope = this, produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ), + ) + ), ) val sessionsSettings = diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt new file mode 100644 index 00000000000..88f1f816c12 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeUuidGenerator.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.sessions.testing + +import com.google.firebase.sessions.UuidGenerator +import java.util.UUID + +/** Fake implementation of [UuidGenerator] to provide uuids of the given names in order. */ +internal class FakeUuidGenerator(private val names: List = listOf(UUID_1, UUID_2, UUID_3)) : + UuidGenerator { + private var index = -1 + + override fun next(): UUID { + index = (index + 1).coerceAtMost(names.size - 1) + return UUID.fromString(names[index]) + } + + companion object { + const val UUID_1 = "11111111-1111-1111-1111-111111111111" + const val UUID_2 = "22222222-2222-2222-2222-222222222222" + const val UUID_3 = "CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC" + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt index eda16d8f0b4..b3431f71840 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeComponent.kt @@ -23,24 +23,46 @@ import com.google.firebase.sessions.FirebaseSessionsComponent import com.google.firebase.sessions.SessionDatastore import com.google.firebase.sessions.SessionFirelogPublisher import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.SessionLifecycleServiceBinder import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsProvider -/** Bridge between FirebaseSessionsComponent and FirebaseSessionsFakeRegistrar. */ +/** Fake component to manage [FirebaseSessions] and related, often faked, dependencies. */ +@Suppress("MemberVisibilityCanBePrivate") // Keep access to fakes open for convenience internal class FirebaseSessionsFakeComponent : FirebaseSessionsComponent { - // TODO(mrober): Move tests to use Dagger for DI. + // TODO(mrober): Move tests that need DI to integration tests, and remove this component. + + // Fakes, access these instances to setup test cases, e.g., add interval to fake time provider. + val fakeTimeProvider = FakeTimeProvider() + val fakeUuidGenerator = FakeUuidGenerator() + val fakeSessionDatastore = FakeSessionDatastore() + val fakeFirelogPublisher = FakeFirelogPublisher() + val fakeSessionLifecycleServiceBinder = FakeSessionLifecycleServiceBinder() + + // Settings providers, default to fake, set these to real instances for relevant test cases. + var localOverrideSettings: SettingsProvider = FakeSettingsProvider() + var remoteSettings: SettingsProvider = FakeSettingsProvider() override val firebaseSessions: FirebaseSessions - get() = Firebase.app[FirebaseSessions::class.java] + get() = throw NotImplementedError("FirebaseSessions not implemented, use integration tests.") + + override val sessionDatastore: SessionDatastore = fakeSessionDatastore + + override val sessionFirelogPublisher: SessionFirelogPublisher = fakeFirelogPublisher - override val sessionDatastore: SessionDatastore - get() = Firebase.app[SessionDatastore::class.java] + override val sessionGenerator: SessionGenerator by lazy { + SessionGenerator(timeProvider = fakeTimeProvider, uuidGenerator = fakeUuidGenerator) + } - override val sessionFirelogPublisher: SessionFirelogPublisher - get() = Firebase.app[SessionFirelogPublisher::class.java] + override val sessionsSettings: SessionsSettings by lazy { + SessionsSettings(localOverrideSettings, remoteSettings) + } - override val sessionGenerator: SessionGenerator - get() = Firebase.app[SessionGenerator::class.java] + val sessionLifecycleServiceBinder: SessionLifecycleServiceBinder + get() = fakeSessionLifecycleServiceBinder - override val sessionsSettings: SessionsSettings - get() = Firebase.app[SessionsSettings::class.java] + companion object { + val instance: FirebaseSessionsFakeComponent + get() = Firebase.app[FirebaseSessionsFakeComponent::class.java] + } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index 58855f622f3..8dc6454931e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -16,85 +16,30 @@ package com.google.firebase.sessions.testing -import androidx.annotation.Keep -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Background -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency -import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.platforminfo.LibraryVersionComponent import com.google.firebase.sessions.BuildConfig import com.google.firebase.sessions.FirebaseSessions import com.google.firebase.sessions.FirebaseSessionsComponent -import com.google.firebase.sessions.SessionDatastore -import com.google.firebase.sessions.SessionFirelogPublisher -import com.google.firebase.sessions.SessionGenerator -import com.google.firebase.sessions.SessionLifecycleServiceBinder -import com.google.firebase.sessions.WallClock -import com.google.firebase.sessions.settings.SessionsSettings -import kotlinx.coroutines.CoroutineDispatcher /** * [ComponentRegistrar] for setting up Fake components for [FirebaseSessions] and its internal * dependencies for unit tests. - * - * @hide */ -@Keep internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { override fun getComponents() = listOf( - Component.builder(SessionGenerator::class.java) - .name("session-generator") - .factory { SessionGenerator(timeProvider = WallClock) } - .build(), - Component.builder(FakeFirelogPublisher::class.java) - .name("fake-session-publisher") - .factory { FakeFirelogPublisher() } - .build(), - Component.builder(SessionFirelogPublisher::class.java) - .name("session-publisher") - .add(Dependency.required(fakeFirelogPublisher)) - .factory { container -> container.get(fakeFirelogPublisher) } - .build(), - Component.builder(SessionsSettings::class.java) - .name("sessions-settings") - .add(Dependency.required(firebaseApp)) - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - SessionsSettings( - container.get(firebaseApp), - container.get(blockingDispatcher), - container.get(backgroundDispatcher), - fakeFirebaseInstallations, - ) - } - .build(), Component.builder(FirebaseSessionsComponent::class.java) - .name("fake-fire-sessions-component") - .factory { FirebaseSessionsFakeComponent() } - .build(), - Component.builder(FakeSessionDatastore::class.java) - .name("fake-sessions-datastore") - .factory { FakeSessionDatastore() } - .build(), - Component.builder(SessionDatastore::class.java) - .name("sessions-datastore") - .add(Dependency.required(fakeDatastore)) - .factory { container -> container.get(fakeDatastore) } + .name("fire-sessions-component") + .add(Dependency.required(firebaseSessionsFakeComponent)) + .factory { container -> container.get(firebaseSessionsFakeComponent) } .build(), - Component.builder(FakeSessionLifecycleServiceBinder::class.java) - .name("fake-sessions-service-binder") - .factory { FakeSessionLifecycleServiceBinder() } - .build(), - Component.builder(SessionLifecycleServiceBinder::class.java) - .name("sessions-service-binder") - .add(Dependency.required(fakeServiceBinder)) - .factory { container -> container.get(fakeServiceBinder) } + Component.builder(FirebaseSessionsFakeComponent::class.java) + .name("fire-sessions-fake-component") + .factory { FirebaseSessionsFakeComponent() } .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) @@ -102,12 +47,6 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { private companion object { const val LIBRARY_NAME = "fire-sessions" - val firebaseApp = unqualified(FirebaseApp::class.java) - val backgroundDispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java) - val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) - val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) - val fakeDatastore = unqualified(FakeSessionDatastore::class.java) - val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) - val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val firebaseSessionsFakeComponent = unqualified(FirebaseSessionsFakeComponent::class.java) } } From 236df1fade20b706b36b13583865788a8f57ad98 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Wed, 19 Mar 2025 09:00:01 -0600 Subject: [PATCH 050/162] Setup kotlinx.serialization plugin (#6783) Setup kotlinx.serialization plugin in the project wide gradle build file. This will be used by AQS. The plugin needs to use the same version as as the kotlin version. --- build.gradle.kts | 1 + gradle/libs.versions.toml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a15ce611215..a10ac0119ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ plugins { id("firebase-ci") id("smoke-tests") alias(libs.plugins.google.services) + alias(libs.plugins.kotlinx.serialization) apply false } extra["targetSdkVersion"] = 34 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4db41dbe493..4c94f0378b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,6 @@ runner = "1.0.2" rxandroid = "2.0.2" rxjava = "2.1.14" serialization = "1.5.1" -serialization-plugin = "1.8.22" slf4jNop = "2.0.9" spotless = "7.0.0.BETA3" testServices = "1.2.0" @@ -231,7 +230,7 @@ maven-resolver = [ [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization-plugin" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } protobuf = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" } errorprone = { id = "net.ltgt.errorprone", version.ref = "gradleErrorpronePlugin" } From af1fe93265a8358fceddb0fa476cfa48f798f6fa Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Wed, 19 Mar 2025 13:15:14 -0400 Subject: [PATCH 051/162] Swap external action to verify changed files for inline code (#6779) In the light of recent security issues, we are choosing to use our own code to replace an external action. See https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised for context b/403703743 --- .github/workflows/release-note-changes.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-note-changes.yml b/.github/workflows/release-note-changes.yml index 06d42153ea4..8d481cdcdad 100644 --- a/.github/workflows/release-note-changes.yml +++ b/.github/workflows/release-note-changes.yml @@ -6,7 +6,7 @@ on: - 'main' jobs: - build: + release-notes-changed: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 @@ -18,12 +18,16 @@ jobs: - name: Get changed changelog files id: changed-files - uses: tj-actions/changed-files@v41.0.0 - with: - files_ignore: | - plugins/** - files: | - **/CHANGELOG.md + run: | + git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha}} | grep CHANGELOG.md > /tmp/changelog_file_list.txt + if [[ "$?" == "0" ]] + then + echo "any_changed=true" >> $GITHUB_OUTPUT + else + echo "any_changed=false" >> $GITHUB_OUTPUT + fi + echo "all_changed_files=$(cat /tmp/changelog_file_list.txt)" >> $GITHUB_OUTPUT + rm /tmp/changelog_file_list.txt - name: Set up JDK 17 uses: actions/setup-java@v4.1.0 From 60a021d7f9e15d84d384895c72adec141d166561 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Wed, 19 Mar 2025 17:41:31 -0400 Subject: [PATCH 052/162] [github actions] Pin actions to hash commits (#6784) Tags can be modified to point to different commits, which is a security issue. By pinning to specific commits we ensure the code executing isn't changing. --- .github/workflows/api-information.yml | 6 +-- .github/workflows/build-release-artifacts.yml | 10 ++-- .github/workflows/changelog.yml | 4 +- .github/workflows/check-head-dependencies.yml | 4 +- .../workflows/check-vertexai-responses.yml | 10 ++-- .github/workflows/check_format.yml | 8 ++-- .github/workflows/ci_tests.yml | 22 ++++----- .github/workflows/config-e2e.yml | 8 ++-- .github/workflows/copyright-check.yml | 4 +- .github/workflows/create_releases.yml | 6 +-- .github/workflows/dataconnect.yml | 24 +++++----- .github/workflows/dataconnect_demo_app.yml | 18 +++---- .github/workflows/diff-javadoc.yml | 6 +-- .github/workflows/fireci.yml | 4 +- .github/workflows/fireperf-e2e.yml | 16 +++---- .github/workflows/firestore_ci_tests.yml | 48 +++++++++---------- .github/workflows/health-metrics.yml | 30 ++++++------ .github/workflows/jekyll-gh-pages.yml | 10 ++-- .github/workflows/make-bom.yml | 12 ++--- .github/workflows/merge-to-main.yml | 2 +- .github/workflows/metalava-semver-check.yml | 6 +-- .github/workflows/plugins-check.yml | 6 +-- .github/workflows/post_release_cleanup.yml | 8 ++-- .github/workflows/private-mirror-sync.yml | 4 +- .github/workflows/release-note-changes.yml | 8 ++-- .github/workflows/scorecards.yml | 4 +- .github/workflows/semver-check.yml | 4 +- .github/workflows/sessions-e2e.yml | 8 ++-- .github/workflows/smoke-tests.yml | 10 ++-- .../workflows/update-cpp-sdk-on-release.yml | 6 +-- .github/workflows/validate-dependencies.yml | 4 +- .github/workflows/version-check.yml | 4 +- 32 files changed, 162 insertions(+), 162 deletions(-) diff --git a/.github/workflows/api-information.yml b/.github/workflows/api-information.yml index f0f1c57d650..df514aa39d5 100644 --- a/.github/workflows/api-information.yml +++ b/.github/workflows/api-information.yml @@ -7,18 +7,18 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - name: Set up fireci diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml index 313226dce97..328deabfdb6 100644 --- a/.github/workflows/build-release-artifacts.yml +++ b/.github/workflows/build-release-artifacts.yml @@ -12,10 +12,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,21 +26,21 @@ jobs: ./gradlew firebasePublish - name: Upload m2 repo - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: m2repository path: build/m2repository/ retention-days: 15 - name: Upload release notes - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: release_notes path: build/release-notes/ retention-days: 15 - name: Upload kotlindocs - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: kotlindocs path: build/firebase-kotlindoc/ diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 7937f67acd5..60660863235 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,11 +13,11 @@ jobs: env: BUNDLE_GEMFILE: ./ci/danger/Gemfile steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 100 submodules: true - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0 with: ruby-version: '2.7' - name: Setup Bundler diff --git a/.github/workflows/check-head-dependencies.yml b/.github/workflows/check-head-dependencies.yml index 088724bf1d4..189b0a0c87c 100644 --- a/.github/workflows/check-head-dependencies.yml +++ b/.github/workflows/check-head-dependencies.yml @@ -10,9 +10,9 @@ jobs: check-head-dependencies: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/check-vertexai-responses.yml b/.github/workflows/check-vertexai-responses.yml index 482254c553d..fd6f009de4c 100644 --- a/.github/workflows/check-vertexai-responses.yml +++ b/.github/workflows/check-vertexai-responses.yml @@ -6,7 +6,7 @@ jobs: check-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Clone mock responses run: firebase-vertexai/update_responses.sh - name: Find cloned and latest versions @@ -17,24 +17,24 @@ jobs: echo "latest_tag=$LATEST" >> $GITHUB_ENV working-directory: firebase-vertexai/src/test/resources/vertexai-sdk-test-data - name: Find comment from previous run if exists - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 id: fc with: issue-number: ${{github.event.number}} body-includes: Vertex AI Mock Responses Check - name: Comment on PR if newer version is available if: ${{env.cloned_tag != env.latest_tag && !steps.fc.outputs.comment-id}} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: issue-number: ${{github.event.number}} body: > ### Vertex AI Mock Responses Check :warning: - + A newer major version of the mock responses for Vertex AI unit tests is available. [update_responses.sh](https://github.com/firebase/firebase-android-sdk/blob/main/firebase-vertexai/update_responses.sh) should be updated to clone the latest version of the responses: `${{env.latest_tag}}` - name: Delete comment when version gets updated if: ${{env.cloned_tag == env.latest_tag && steps.fc.outputs.comment-id}} - uses: detomarco/delete-comment@850734dd44d8b15fef55b45252613b903ceb06f0 + uses: detomarco/delete-comment@dd37d1026c669ebfb0ffa5d23890010759ff05d5 # v1.1.0 with: comment-id: ${{ steps.fc.outputs.comment-id }} diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index 6bdfb0ea4d1..83fdc3ec605 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,13 +44,13 @@ jobs: module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 603719e8142..49721333ac8 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,13 +44,13 @@ jobs: module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -76,7 +76,7 @@ jobs: MODULE=${{matrix.module}} echo "ARTIFACT_NAME=${MODULE//:/_}" >> $GITHUB_ENV - name: Upload Test Results - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: always() with: name: unit-test-result-${{env.ARTIFACT_NAME}} @@ -113,13 +113,13 @@ jobs: - module: :firebase-functions:ktx steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -130,10 +130,10 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: ${{ matrix.module }} Integ Tests env: FIREBASE_CI: 1 @@ -159,11 +159,11 @@ jobs: steps: - name: Download Artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@82082dac68ad6a19d980f8ce817e108b9f496c2a + uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 with: files: "artifacts/**/*.xml" diff --git a/.github/workflows/config-e2e.yml b/.github/workflows/config-e2e.yml index 604115b324d..15091c2d3f9 100644 --- a/.github/workflows/config-e2e.yml +++ b/.github/workflows/config-e2e.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout firebase-config - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: '17' distribution: 'temurin' @@ -31,10 +31,10 @@ jobs: run: | echo $REMOTE_CONFIG_E2E_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_service_account }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Run Remote Config end-to-end tests env: FTL_RESULTS_BUCKET: fireescape diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index b9e3aeba227..4f90b26f7f6 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -10,8 +10,8 @@ jobs: copyright-check: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.9' - run: | diff --git a/.github/workflows/create_releases.yml b/.github/workflows/create_releases.yml index df4159cd285..0da1384927e 100644 --- a/.github/workflows/create_releases.yml +++ b/.github/workflows/create_releases.yml @@ -32,11 +32,11 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -46,7 +46,7 @@ jobs: ./gradlew generateReleaseConfig -PcurrentRelease=${{ inputs.name }} -PpastRelease=${{ inputs.past-name }} -PprintOutput=true - name: Create Pull Request - uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: base: 'releases/${{ inputs.name }}' branch: 'releases/${{ inputs.name }}.release' diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml index 797f112fd61..9fa9511d717 100644 --- a/.github/workflows/dataconnect.yml +++ b/.github/workflows/dataconnect.yml @@ -51,16 +51,16 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: show-progress: false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin - - uses: actions/setup-node@v4 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ env.FDC_NODEJS_VERSION }} @@ -74,7 +74,7 @@ jobs: - name: Restore Gradle cache id: restore-gradle-cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name != 'schedule' with: path: | @@ -118,7 +118,7 @@ jobs: :firebase-dataconnect:assembleDebugAndroidTest - name: Save Gradle cache - uses: actions/cache/save@v4 + uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name == 'schedule' with: path: | @@ -134,7 +134,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Restore AVD cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name != 'schedule' id: restore-avd-cache with: @@ -147,7 +147,7 @@ jobs: - name: Create AVD if: github.event_name == 'schedule' || steps.restore-avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 with: api-level: ${{ env.FDC_ANDROID_EMULATOR_API_LEVEL }} arch: x86_64 @@ -157,7 +157,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Save AVD cache - uses: actions/cache/save@v4 + uses: actions/cache/save@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 if: github.event_name == 'schedule' with: path: | @@ -187,7 +187,7 @@ jobs: - name: Gradle connectedCheck id: connectedCheck - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 # Allow this GitHub Actions "job" to continue even if the tests fail so that logs from a # failed test run get uploaded as "artifacts" and are available to investigate failed runs. # A later step in this "job" will fail the job if this step fails @@ -202,7 +202,7 @@ jobs: set -eux && ./gradlew ${{ (inputs.gradleInfoLog && '--info') || '' }} :firebase-dataconnect:connectedCheck :firebase-dataconnect:connectors:connectedCheck - name: Upload log file artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: integration_test_logs path: "**/*.log" @@ -210,7 +210,7 @@ jobs: compression-level: 9 - name: Upload Gradle build report artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: integration_test_gradle_build_reports path: firebase-dataconnect/**/build/reports/ @@ -230,7 +230,7 @@ jobs: continue-on-error: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: show-progress: false - uses: docker://rhysd/actionlint:1.7.7 diff --git a/.github/workflows/dataconnect_demo_app.yml b/.github/workflows/dataconnect_demo_app.yml index c401f296b71..35a5079c96e 100644 --- a/.github/workflows/dataconnect_demo_app.yml +++ b/.github/workflows/dataconnect_demo_app.yml @@ -36,7 +36,7 @@ jobs: continue-on-error: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: sparse-checkout: firebase-dataconnect/demo @@ -45,7 +45,7 @@ jobs: echo "gmagjr2b9d" >github_actions_demo_test_cache_key.txt echo "${{ env.FDC_FIREBASE_TOOLS_VERSION }}" >github_actions_demo_assemble_firebase_tools_version.txt - - uses: actions/setup-node@v3 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: ${{ env.FDC_NODE_VERSION }} cache: 'npm' @@ -55,7 +55,7 @@ jobs: - name: cache package-lock.json id: package_json_lock - uses: actions/cache@v4 + uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # 4.2.2 with: path: ${{ env.FDC_FIREBASE_TOOLS_DIR }}/package*.json key: firebase_tools_package_json-${{ env.FDC_FIREBASE_TOOLS_VERSION }} @@ -73,9 +73,9 @@ jobs: if: steps.package_json_lock.outputs.cache-hit == 'true' run: | cd ${{ env.FDC_FIREBASE_TOOLS_DIR }} - npm ci --fund=false --audit=false + npm ci --fund=false --audit=false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin @@ -114,14 +114,14 @@ jobs: -PdataConnect.minimalApp.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \ assemble test - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: apks path: firebase-dataconnect/demo/build/**/*.apk if-no-files-found: warn compression-level: 0 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: gradle_build_reports path: firebase-dataconnect/demo/build/reports/ @@ -132,14 +132,14 @@ jobs: continue-on-error: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: sparse-checkout: firebase-dataconnect/demo - name: Create Cache Key Files run: echo "h99ee4egfd" >github_actions_demo_spotless_cache_key.txt - - uses: actions/setup-java@v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: ${{ env.FDC_JAVA_VERSION }} distribution: temurin diff --git a/.github/workflows/diff-javadoc.yml b/.github/workflows/diff-javadoc.yml index c780e07c714..db25e1e5281 100644 --- a/.github/workflows/diff-javadoc.yml +++ b/.github/workflows/diff-javadoc.yml @@ -13,13 +13,13 @@ jobs: run: mkdir ~/diff - name: Checkout PR branch - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -32,7 +32,7 @@ jobs: run: mv build ~/diff/modified - name: Checkout main - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.base_ref }} diff --git a/.github/workflows/fireci.yml b/.github/workflows/fireci.yml index b375d6bb93d..3748428b64d 100644 --- a/.github/workflows/fireci.yml +++ b/.github/workflows/fireci.yml @@ -15,8 +15,8 @@ jobs: name: "fireci tests" runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4.1.1 - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.8' - run: | diff --git a/.github/workflows/fireperf-e2e.yml b/.github/workflows/fireperf-e2e.yml index 9299ba57000..3be2e162654 100644 --- a/.github/workflows/fireperf-e2e.yml +++ b/.github/workflows/fireperf-e2e.yml @@ -20,29 +20,29 @@ jobs: environment: [ prod, autopush ] steps: - name: Checkout firebase-android-sdk - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Checkout firebase-android-buildtools - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: FirebasePrivate/firebase-android-buildtools token: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} path: firebase-android-buildtools - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - name: Set up fireci run: pip3 install -e ci/fireci - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Add google-services.json run: echo $PERF_E2E_GOOGLE_SERVICES | base64 -d > google-services.json - name: Run fireperf end-to-end tests @@ -52,7 +52,7 @@ jobs: --target_environment=${{ matrix.environment }} - name: Notify developers upon failures if: ${{ failure() }} - uses: actions/github-script@v6 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const owner = context.repo.owner; @@ -98,7 +98,7 @@ jobs: } - name: Upload test artifacts if: always() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: test-artifacts (${{ matrix.environment }}) path: | diff --git a/.github/workflows/firestore_ci_tests.yml b/.github/workflows/firestore_ci_tests.yml index 00ce91b4e92..a7ea11b1624 100644 --- a/.github/workflows/firestore_ci_tests.yml +++ b/.github/workflows/firestore_ci_tests.yml @@ -16,13 +16,13 @@ jobs: outputs: modules: ${{ steps.changed-modules.outputs.modules }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -44,7 +44,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -53,10 +53,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -67,12 +67,12 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: firebase-firestore Integ Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -88,7 +88,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="prod" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: logcat.txt path: logcat.txt @@ -107,7 +107,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -116,10 +116,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -130,14 +130,14 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 # create composite indexes with Terraform - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 - name: Terraform Init run: | cd firebase-firestore @@ -164,7 +164,7 @@ jobs: - name: Firestore Named DB Integ Tests timeout-minutes: 20 - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -180,7 +180,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="prod" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: named-db-logcat.txt path: logcat.txt @@ -198,7 +198,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true @@ -207,10 +207,10 @@ jobs: run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -221,13 +221,13 @@ jobs: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.NIGHTLY_INTEG_TESTS_GOOGLE_SERVICES }} run: | echo $INTEG_TESTS_GOOGLE_SERVICES > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Firestore Nightly Integ Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 env: FIREBASE_CI: 1 FTL_RESULTS_BUCKET: android-ci @@ -243,7 +243,7 @@ jobs: ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="nightly" - name: Upload logs if: failure() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: nightly-logcat.txt path: logcat.txt diff --git a/.github/workflows/health-metrics.yml b/.github/workflows/health-metrics.yml index 0b20dcd1078..9e086be9c3a 100644 --- a/.github/workflows/health-metrics.yml +++ b/.github/workflows/health-metrics.yml @@ -24,24 +24,24 @@ jobs: && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Run coverage tests (presubmit) @@ -59,24 +59,24 @@ jobs: && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Run size tests (presubmit) @@ -95,24 +95,24 @@ jobs: && github.event.pull_request.base.ref == 'main') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Set up fireci run: pip3 install -e ci/fireci - name: Add google-services.json diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml index c1683b58de8..077b5b465b2 100644 --- a/.github/workflows/jekyll-gh-pages.yml +++ b/.github/workflows/jekyll-gh-pages.yml @@ -31,16 +31,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Pages - uses: actions/configure-pages@v2 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Build with Jekyll - uses: actions/jekyll-build-pages@v1 + uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13 with: source: ./contributor-docs destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa #v3.0.1 deploy: if: ${{ github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk' }} @@ -52,4 +52,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e #v4.0.5 diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml index 0e7d63f5c96..4643217a528 100644 --- a/.github/workflows/make-bom.yml +++ b/.github/workflows/make-bom.yml @@ -8,14 +8,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: '3.10' - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,21 +26,21 @@ jobs: ./gradlew buildBomBundleZip - name: Upload bom - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: bom path: build/bom/ retention-days: 15 - name: Upload release notes - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: bom_release_notes path: build/bomReleaseNotes.md retention-days: 15 - name: Upload recipe version update - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: recipe_version path: build/recipeVersionUpdate.txt diff --git a/.github/workflows/merge-to-main.yml b/.github/workflows/merge-to-main.yml index 4df37c57891..2d08b177208 100644 --- a/.github/workflows/merge-to-main.yml +++ b/.github/workflows/merge-to-main.yml @@ -15,7 +15,7 @@ jobs: permissions: pull-requests: write steps: - - uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 with: message: > ### 📝 PRs merging into main branch diff --git a/.github/workflows/metalava-semver-check.yml b/.github/workflows/metalava-semver-check.yml index df68a691234..0c196eeef89 100644 --- a/.github/workflows/metalava-semver-check.yml +++ b/.github/workflows/metalava-semver-check.yml @@ -10,12 +10,12 @@ jobs: pull-requests: write steps: - name: Checkout main - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.base_ref }} - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -25,7 +25,7 @@ jobs: run: ./gradlew copyApiTxtFile - name: Checkout PR - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.head_ref }} clean: false diff --git a/.github/workflows/plugins-check.yml b/.github/workflows/plugins-check.yml index fa482c36d35..6ebdb8044bd 100644 --- a/.github/workflows/plugins-check.yml +++ b/.github/workflows/plugins-check.yml @@ -13,9 +13,9 @@ jobs: plugins-check: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,7 +26,7 @@ jobs: run: | ./gradlew plugins:check - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@82082dac68ad6a19d980f8ce817e108b9f496c2a + uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 with: files: "**/build/test-results/**/*.xml" check_name: "plugins test results" diff --git a/.github/workflows/post_release_cleanup.yml b/.github/workflows/post_release_cleanup.yml index 8206b735a11..d7ee562bb51 100644 --- a/.github/workflows/post_release_cleanup.yml +++ b/.github/workflows/post_release_cleanup.yml @@ -12,11 +12,11 @@ jobs: create-pull-request: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin @@ -26,7 +26,7 @@ jobs: ./gradlew postReleaseCleanup - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ secrets.GOOGLE_OSS_BOT_TOKEN }} committer: google-oss-bot @@ -41,6 +41,6 @@ jobs: title: '${{ inputs.name}} mergeback' body: | Auto-generated PR for cleaning up release ${{ inputs.name}} - + NO_RELEASE_CHANGE commit-message: 'Post release cleanup for ${{ inputs.name }}' diff --git a/.github/workflows/private-mirror-sync.yml b/.github/workflows/private-mirror-sync.yml index 324993eb791..dc17fb289cc 100644 --- a/.github/workflows/private-mirror-sync.yml +++ b/.github/workflows/private-mirror-sync.yml @@ -10,14 +10,14 @@ jobs: if: github.repository == 'FirebasePrivate/firebase-android-sdk' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: firebase/firebase-android-sdk ref: main fetch-depth: 0 submodules: true - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 submodules: true diff --git a/.github/workflows/release-note-changes.yml b/.github/workflows/release-note-changes.yml index 8d481cdcdad..95debd4469e 100644 --- a/.github/workflows/release-note-changes.yml +++ b/.github/workflows/release-note-changes.yml @@ -9,7 +9,7 @@ jobs: release-notes-changed: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -30,14 +30,14 @@ jobs: rm /tmp/changelog_file_list.txt - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - name: Set up Python 3.10 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 if: ${{ steps.changed-files.outputs.any_changed == 'true' }} with: python-version: '3.10' @@ -54,7 +54,7 @@ jobs: fireci changelog_comment -c "${{ steps.changed-files.outputs.all_changed_files }}" -o ./changelog_comment.md - name: Add PR Comment - uses: mshick/add-pr-comment@v2.8.1 + uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2 continue-on-error: true with: status: ${{ steps.generate-comment.outcome }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 22bd7f8e3c2..ed18d8c2a2c 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -73,7 +73,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: Upload artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/semver-check.yml b/.github/workflows/semver-check.yml index 2fc7eb38843..77b528b936b 100644 --- a/.github/workflows/semver-check.yml +++ b/.github/workflows/semver-check.yml @@ -10,9 +10,9 @@ jobs: semver-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/sessions-e2e.yml b/.github/workflows/sessions-e2e.yml index 048cd92eee9..092a51fc094 100644 --- a/.github/workflows/sessions-e2e.yml +++ b/.github/workflows/sessions-e2e.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout firebase-sessions - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: '11' distribution: 'temurin' @@ -31,10 +31,10 @@ jobs: run: | echo $SESSIONS_E2E_GOOGLE_SERVICES | base64 -d > google-services.json - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 - name: Run sessions end-to-end tests env: FTL_RESULTS_BUCKET: fireescape diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 07ab7dbeeb2..d39d6ab6562 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -7,20 +7,20 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 submodules: true - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin cache: gradle - - uses: google-github-actions/auth@v2 + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 # TODO(yifany): make it a fireci plugin and remove the separately distributed jar file - name: Download smoke tests runner @@ -51,7 +51,7 @@ jobs: - name: Upload test artifacts if: always() - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: smoke-tests-artifacts path: | diff --git a/.github/workflows/update-cpp-sdk-on-release.yml b/.github/workflows/update-cpp-sdk-on-release.yml index 60ffbc47285..49e6b0e1392 100644 --- a/.github/workflows/update-cpp-sdk-on-release.yml +++ b/.github/workflows/update-cpp-sdk-on-release.yml @@ -23,7 +23,7 @@ jobs: outputs: released_version_changed: ${{ steps.check_version.outputs.released_version_changed }} steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Check out the actual head commit, not any merge commit. ref: ${{ github.sha }} @@ -51,12 +51,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: python-version: 3.7 - name: Check out firebase-cpp-sdk - uses: actions/checkout@v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: firebase/firebase-cpp-sdk ref: main diff --git a/.github/workflows/validate-dependencies.yml b/.github/workflows/validate-dependencies.yml index c91ad8aee0c..b6fe70c5133 100644 --- a/.github/workflows/validate-dependencies.yml +++ b/.github/workflows/validate-dependencies.yml @@ -10,9 +10,9 @@ jobs: build-artifacts: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index f5f285e29a0..7824404d362 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -10,9 +10,9 @@ jobs: version-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up JDK 17 - uses: actions/setup-java@v4.1.0 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 17 distribution: temurin From d0dc250676ca5f1a28030df1a54f5d70df31a5c4 Mon Sep 17 00:00:00 2001 From: Daymon <17409137+daymxn@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:41:17 -0500 Subject: [PATCH 053/162] Add transform action to remove package prefix in toc files (#6787) Per [b/379093944](https://b.corp.google.com/issues/379093944), This adds a transform action to remove the `com.google.` package prefix in the TOC files, as it pollutes the visual landscape for consumers. Long-term, this action will be replaced by a parameter offered via newer versions of dackka- but we don't currently have the bandwidth for such an upgrade. A tracking bug has been left with the implementation to migrate to said parameter whenever we have the time to upgrade. --- .../gradle/plugins/FiresiteTransformTask.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt index e8f4186d97e..7de416de561 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt @@ -38,6 +38,7 @@ import org.gradle.api.tasks.TaskAction * - Adds the deprecated status to ktx sections in _toc.yaml files * - Fixes broken hyperlinks in `@see` blocks * - Removes the prefix path from book_path + * - Removes the `com.google` package prefix from _toc.yaml files * * **Please note:** This task is idempotent- meaning it can safely be ran multiple times on the same * set of files. @@ -77,10 +78,35 @@ abstract class FiresiteTransformTask : DefaultTask() { } private fun File.fixYamlFile() { - val fixedContent = readText().removeClassHeader().removeIndexHeader().addDeprecatedStatus() + val fixedContent = + readText().removeClassHeader().removeIndexHeader().addDeprecatedStatus().removePackagePrefix() writeText(fixedContent) } + /** + * Removes the `com.google.` prefix from the package titles in the table of contents. + * + * The prefix pollutes the TOC, especially on smaller screen sizes; so we opt to removed it + * entirely. + * + * Example input: + * ``` + * toc: + * - title: "com.google.firebase.functions" + * path: "/docs/reference/android/com/google/firebase/functions/package-summary.html" + * ``` + * + * Example output: + * ``` + * toc: + * - title: "firebase.functions" + * path: "/docs/reference/android/com/google/firebase/functions/package-summary.html" + * ``` + * + * TODO(b/378717454): Migrate to the param packagePrefixToRemoveInToc in dackka when fixed + */ + private fun String.removePackagePrefix() = remove(Regex("(?<=title: \")(com\\.google\\.)")) + /** * Fixes broken hyperlinks in the rendered HTML * From 49dd6e08c291d92fab7183e26d354ba3284de433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?= Date: Fri, 21 Mar 2025 13:24:02 +0000 Subject: [PATCH 054/162] include the full googleid module in GenerateTutorialBundleTask.kt (#6789) I suspect this is what is causing the plugin to try and remove the `com.google.android.libraries.identity.googleid:googleid:1.1.1` library. (as seen in [cl/738906664](https://critique.corp.google.com/cl/738906664/depot/google3/third_party/devsite/firebase/en/android-studio/firebase_assistant/firebase_tutorial_bundle.xml?version=r201#43)) --- .../firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt index e99565d47f7..6b2ebf15f9a 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateTutorialBundleTask.kt @@ -261,7 +261,7 @@ abstract class GenerateTutorialBundleTask : DefaultTask() { ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-first-dependency"), "androidx.credentials:credentials-play-services-auth" to ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-second-dependency"), - "com.google.android.libraries.identity.googleid" to + "com.google.android.libraries.identity.googleid:googleid" to ArtifactTutorialMapping("Auth Google Sign In", "auth-google-signin-third-dependency"), "com.google.firebase:firebase-appdistribution-gradle" to ArtifactTutorialMapping( From 51a9b1d446eac91e776863424f16b298969dc9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?= Date: Fri, 21 Mar 2025 14:12:57 +0000 Subject: [PATCH 055/162] ci: add missing permissions to .github/workflows/plugins-check.yml (#6790) This might be the cause of https://github.com/firebase/firebase-android-sdk/actions/runs/13979443579/job/39141224363?pr=6789 Update: confirmed in https://github.com/firebase/firebase-android-sdk/actions/runs/13979884815/job/39142572824?pr=6790 --------- Co-authored-by: Rodrigo Lazo --- .github/workflows/plugins-check.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/plugins-check.yml b/.github/workflows/plugins-check.yml index 6ebdb8044bd..3cbb6d2d01b 100644 --- a/.github/workflows/plugins-check.yml +++ b/.github/workflows/plugins-check.yml @@ -11,6 +11,9 @@ concurrency: jobs: plugins-check: + permissions: + checks: write + pull-requests: write runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 2d5cabed7c0ee6531c3549953698937b8b5736bb Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 21 Mar 2025 10:57:49 -0400 Subject: [PATCH 056/162] dataconnect: remove "beta" version marker and graduate the data connect sdk from "beta" to "generally available" (#6792) --- firebase-dataconnect/CHANGELOG.md | 3 +++ firebase-dataconnect/gradle.properties | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index b21ac994950..cddd73f72b8 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased +* [changed] Removed the "beta" suffix from the version of the Firebase Data + Connect Android SDK, thus graduating it from "beta" to "generally available". + ([#6792](https://github.com/firebase/firebase-android-sdk/pull/6792)) * [changed] Changed gRPC proto package to v1 (was v1beta). ([#6729](https://github.com/firebase/firebase-android-sdk/pull/6729)) diff --git a/firebase-dataconnect/gradle.properties b/firebase-dataconnect/gradle.properties index 6cf883fd07e..e4c40384b6c 100644 --- a/firebase-dataconnect/gradle.properties +++ b/firebase-dataconnect/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.0-beta05 +version=16.0.0 latestReleasedVersion=16.0.0-beta04 From f9d18022a0c3c64b7451602f7dba8643b42be310 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 21 Mar 2025 14:58:28 -0700 Subject: [PATCH 057/162] m161 mergeback (#6793) Auto-generated PR for cleaning up release m161 NO_RELEASE_CHANGE Co-authored-by: emilypgoogle --- firebase-appdistribution-api/CHANGELOG.md | 10 ++++++++++ firebase-appdistribution-api/gradle.properties | 4 ++-- firebase-appdistribution/CHANGELOG.md | 3 +++ firebase-appdistribution/gradle.properties | 4 ++-- firebase-crashlytics-ndk/CHANGELOG.md | 3 +++ firebase-crashlytics-ndk/gradle.properties | 4 ++-- firebase-crashlytics/CHANGELOG.md | 8 ++++++++ firebase-crashlytics/gradle.properties | 4 ++-- firebase-dataconnect/CHANGELOG.md | 6 +++--- firebase-dataconnect/gradle.properties | 4 ++-- firebase-firestore/CHANGELOG.md | 8 ++++++++ firebase-firestore/gradle.properties | 4 ++-- firebase-functions/CHANGELOG.md | 8 ++++++++ firebase-functions/gradle.properties | 4 ++-- firebase-inappmessaging-display/CHANGELOG.md | 8 ++++++++ firebase-inappmessaging-display/gradle.properties | 4 ++-- firebase-inappmessaging/CHANGELOG.md | 8 ++++++++ firebase-inappmessaging/gradle.properties | 4 ++-- firebase-messaging-directboot/CHANGELOG.md | 3 +++ firebase-messaging-directboot/gradle.properties | 4 ++-- firebase-messaging/CHANGELOG.md | 8 ++++++++ firebase-messaging/gradle.properties | 4 ++-- firebase-perf/CHANGELOG.md | 8 ++++++++ firebase-perf/gradle.properties | 4 ++-- firebase-sessions/CHANGELOG.md | 8 ++++++++ firebase-sessions/gradle.properties | 4 ++-- protolite-well-known-types/CHANGELOG.md | 9 +++++++++ protolite-well-known-types/gradle.properties | 4 ++-- 28 files changed, 123 insertions(+), 31 deletions(-) diff --git a/firebase-appdistribution-api/CHANGELOG.md b/firebase-appdistribution-api/CHANGELOG.md index 52fc8534ef5..44afcf8054e 100644 --- a/firebase-appdistribution-api/CHANGELOG.md +++ b/firebase-appdistribution-api/CHANGELOG.md @@ -1,6 +1,16 @@ # Unreleased +# 16.0.0-beta15 +* [unchanged] Updated to accommodate the release of the updated + [appdistro] library. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-appdistribution-api` library. The Kotlin extensions library has no additional +updates. + # 16.0.0-beta14 * [unchanged] Updated to accommodate the release of the updated [appdistro] library. diff --git a/firebase-appdistribution-api/gradle.properties b/firebase-appdistribution-api/gradle.properties index 43fb0b20a81..a39a1d388f4 100644 --- a/firebase-appdistribution-api/gradle.properties +++ b/firebase-appdistribution-api/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta15 -latestReleasedVersion=16.0.0-beta14 +version=16.0.0-beta16 +latestReleasedVersion=16.0.0-beta15 diff --git a/firebase-appdistribution/CHANGELOG.md b/firebase-appdistribution/CHANGELOG.md index 3bfc9628fd5..c7ef8338657 100644 --- a/firebase-appdistribution/CHANGELOG.md +++ b/firebase-appdistribution/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased + + +# 16.0.0-beta15 * [fixed] Added custom tab support for more browsers [#6692] # 16.0.0-beta14 diff --git a/firebase-appdistribution/gradle.properties b/firebase-appdistribution/gradle.properties index d02dfc98183..5dcfcbcc66c 100644 --- a/firebase-appdistribution/gradle.properties +++ b/firebase-appdistribution/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta15 -latestReleasedVersion=16.0.0-beta14 +version=16.0.0-beta16 +latestReleasedVersion=16.0.0-beta15 diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index 237ce272b14..dc276b94fc5 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased + + +# 19.4.2 * [changed] Updated `firebase-crashlytics` dependency to v19.4.2 # 19.4.1 diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties index a7ea562fe0f..5e690a3661f 100644 --- a/firebase-crashlytics-ndk/gradle.properties +++ b/firebase-crashlytics-ndk/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.2 -latestReleasedVersion=19.4.1 +version=19.4.3 +latestReleasedVersion=19.4.2 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 923fc710695..65f3b86f462 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased + + +# 19.4.2 * [changed] Internal changes to read version control info more efficiently [#6754] * [fixed] Fixed NoSuchMethodError when getting process info on Android 13 on some devices [#6720] * [changed] Updated `firebase-sessions` dependency to v2.1.0 @@ -6,6 +9,11 @@ * [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8) [#6688] +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + # 19.4.1 * [changed] Updated `firebase-sessions` dependency to v2.0.9 diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties index a7ea562fe0f..5e690a3661f 100644 --- a/firebase-crashlytics/gradle.properties +++ b/firebase-crashlytics/gradle.properties @@ -1,2 +1,2 @@ -version=19.4.2 -latestReleasedVersion=19.4.1 +version=19.4.3 +latestReleasedVersion=19.4.2 diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index cddd73f72b8..02a3f1c836f 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased -* [changed] Removed the "beta" suffix from the version of the Firebase Data - Connect Android SDK, thus graduating it from "beta" to "generally available". - ([#6792](https://github.com/firebase/firebase-android-sdk/pull/6792)) + + +# 16.0.0-beta05 * [changed] Changed gRPC proto package to v1 (was v1beta). ([#6729](https://github.com/firebase/firebase-android-sdk/pull/6729)) diff --git a/firebase-dataconnect/gradle.properties b/firebase-dataconnect/gradle.properties index e4c40384b6c..e9c28e87f88 100644 --- a/firebase-dataconnect/gradle.properties +++ b/firebase-dataconnect/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.0 -latestReleasedVersion=16.0.0-beta04 +version=16.0.0-beta06 +latestReleasedVersion=16.0.0-beta05 diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 939f70c93bb..745b4905503 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,8 +1,16 @@ # Unreleased + + +# 25.1.3 * [fixed] Use lazy encoding in UTF-8 encoded byte comparison for strings to solve performance issues. [#6706](//github.com/firebase/firebase-android-sdk/pull/6706) * [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-firestore` library. The Kotlin extensions library has no additional +updates. + # 25.1.2 * [fixed] Fixed a server and sdk mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615) diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties index baa5399b1dc..fef34db2530 100644 --- a/firebase-firestore/gradle.properties +++ b/firebase-firestore/gradle.properties @@ -1,2 +1,2 @@ -version=25.1.3 -latestReleasedVersion=25.1.2 +version=25.1.4 +latestReleasedVersion=25.1.3 diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 7b0f4e03b6d..7bb63f1f666 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,8 +1,16 @@ # Unreleased + + +# 21.2.0 * [feature] Streaming callable functions are now supported. * [fixed] Fixed an issue that prevented the App Check token from being handled correctly in case of error. +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. + # 21.1.1 * [fixed] Resolve Kotlin migration visibility issues ([#6522](//github.com/firebase/firebase-android-sdk/pull/6522)) diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index 4e8c15934dd..1f3640c14dc 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=21.2.0 -latestReleasedVersion=21.1.1 +version=21.2.1 +latestReleasedVersion=21.2.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-inappmessaging-display/CHANGELOG.md b/firebase-inappmessaging-display/CHANGELOG.md index a9b37cf7f10..706aad30b1d 100644 --- a/firebase-inappmessaging-display/CHANGELOG.md +++ b/firebase-inappmessaging-display/CHANGELOG.md @@ -1,7 +1,15 @@ # Unreleased + + +# 21.0.2 * [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-inappmessaging-display` library. The Kotlin extensions library has no additional +updates. + # 21.0.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/firebase-inappmessaging-display/gradle.properties b/firebase-inappmessaging-display/gradle.properties index 4ad037837d4..f8301e25bcc 100644 --- a/firebase-inappmessaging-display/gradle.properties +++ b/firebase-inappmessaging-display/gradle.properties @@ -1,2 +1,2 @@ -version=21.0.2 -latestReleasedVersion=21.0.1 +version=21.0.3 +latestReleasedVersion=21.0.2 diff --git a/firebase-inappmessaging/CHANGELOG.md b/firebase-inappmessaging/CHANGELOG.md index 1252d73f787..c6f88c8e69d 100644 --- a/firebase-inappmessaging/CHANGELOG.md +++ b/firebase-inappmessaging/CHANGELOG.md @@ -1,7 +1,15 @@ # Unreleased + + +# 21.0.2 * [changed] Updated `protolite-well-known-types` dependency to `18.0.1`. [#6716] +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-inappmessaging` library. The Kotlin extensions library has no additional +updates. + # 21.0.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/firebase-inappmessaging/gradle.properties b/firebase-inappmessaging/gradle.properties index 4ad037837d4..f8301e25bcc 100644 --- a/firebase-inappmessaging/gradle.properties +++ b/firebase-inappmessaging/gradle.properties @@ -1,2 +1,2 @@ -version=21.0.2 -latestReleasedVersion=21.0.1 +version=21.0.3 +latestReleasedVersion=21.0.2 diff --git a/firebase-messaging-directboot/CHANGELOG.md b/firebase-messaging-directboot/CHANGELOG.md index ea728d95e54..1d9568c4f09 100644 --- a/firebase-messaging-directboot/CHANGELOG.md +++ b/firebase-messaging-directboot/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased +# 24.1.1 +* [unchanged] Updated to keep messaging SDK versions aligned. + # 24.1.0 * [unchanged] Updated to keep messaging SDK versions aligned. diff --git a/firebase-messaging-directboot/gradle.properties b/firebase-messaging-directboot/gradle.properties index 11e55c591b5..23127f4cada 100644 --- a/firebase-messaging-directboot/gradle.properties +++ b/firebase-messaging-directboot/gradle.properties @@ -1,3 +1,3 @@ -version=24.1.1 -latestReleasedVersion=24.1.0 +version=24.1.2 +latestReleasedVersion=24.1.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index 6b0a8bdadd9..5ea1053e78a 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,8 +1,16 @@ # Unreleased + + +# 24.1.1 * [changed] Bug fix in SyncTask to always unregister the receiver on the same context on which it was registered. +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-messaging` library. The Kotlin extensions library has no additional +updates. + # 24.1.0 * [deprecated] Deprecated additional FCM upstream messaging methods and updated all upstream methods to indicate they are now decommissioned. See the diff --git a/firebase-messaging/gradle.properties b/firebase-messaging/gradle.properties index 11e55c591b5..23127f4cada 100644 --- a/firebase-messaging/gradle.properties +++ b/firebase-messaging/gradle.properties @@ -1,3 +1,3 @@ -version=24.1.1 -latestReleasedVersion=24.1.0 +version=24.1.2 +latestReleasedVersion=24.1.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 69cb25eeae3..dabc2485b29 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,8 +1,16 @@ # Unreleased + + +# 21.0.5 * [changed] Updated `protolite-well-known-types` dependency to v18.0.1 [#6716] * [fixed] Fixed a bug that allowed invalid payload bytes value in network request metrics [#6721] +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-performance` library. The Kotlin extensions library has no additional +updates. + # 21.0.4 * [fixed] Fixed a performance issue with shared preferences calling `.apply()` every time a value is read from remote config (#6407) diff --git a/firebase-perf/gradle.properties b/firebase-perf/gradle.properties index 4b2de75bc47..beb8d9bb532 100644 --- a/firebase-perf/gradle.properties +++ b/firebase-perf/gradle.properties @@ -15,7 +15,7 @@ # # -version=21.0.5 -latestReleasedVersion=21.0.4 +version=21.0.6 +latestReleasedVersion=21.0.5 android.enableUnitTestBinaryResources=true diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index fb7165f1cfd..8353fbf9029 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,10 +1,18 @@ # Unreleased + + +# 2.1.0 * [changed] Add warning for known issue b/328687152 * [changed] Use Dagger for dependency injection * [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-sessions` library. The Kotlin extensions library has no additional +updates. + # 2.0.9 * [fixed] Make AQS resilient to background init in multi-process apps. diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties index 6faf5922ee1..b81f9766b9f 100644 --- a/firebase-sessions/gradle.properties +++ b/firebase-sessions/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=2.1.0 -latestReleasedVersion=2.0.9 +version=2.1.1 +latestReleasedVersion=2.1.0 diff --git a/protolite-well-known-types/CHANGELOG.md b/protolite-well-known-types/CHANGELOG.md index 9947693ea08..c96214ca169 100644 --- a/protolite-well-known-types/CHANGELOG.md +++ b/protolite-well-known-types/CHANGELOG.md @@ -1,3 +1,12 @@ # Unreleased + + +# 18.0.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). + +## Kotlin +The Kotlin extensions library transitively includes the updated +`protolite-well-known-types` library. The Kotlin extensions library has no additional +updates. + diff --git a/protolite-well-known-types/gradle.properties b/protolite-well-known-types/gradle.properties index d7239d0c4fe..44093f1682d 100644 --- a/protolite-well-known-types/gradle.properties +++ b/protolite-well-known-types/gradle.properties @@ -1,5 +1,5 @@ # IMPORTANT (b/285892320) Keep version and latestReleasedVersion in sync # unless you are releasing a new version of the library to prevent issues # with transitive dependencies. -version=18.0.1 -latestReleasedVersion=18.0.0 +version=18.0.2 +latestReleasedVersion=18.0.1 From 615352d68db776bdde40fff1a1aeea851ee8d6bc Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Mar 2025 10:48:23 -0400 Subject: [PATCH 058/162] [Functions] Several functions improvements (#6796) These improvements are both in the SDK itself, and in the code used for the actual functions running in the cloud during integration testing: A summary of the changes is: - [Backend functions] Update the dependencies and node runtime version - [Backend functions] Add logging in case the client does not support streaming - [SDK] Use a more robust handle of the media type - [SDK] Fix issue introduced in #6773 that missed a return in case of error - [SDK] Other minor code fixes b/404814020 --- firebase-functions/CHANGELOG.md | 3 +- .../androidTest/backend/functions/index.js | 5 ++++ .../backend/functions/package.json | 6 ++-- .../google/firebase/functions/StreamTests.kt | 3 +- .../firebase/functions/PublisherStream.kt | 30 ++++++++++++++----- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 7bb63f1f666..9b31496df74 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [fixed] Fixed issue that caused the SDK to crash when trying to stream a function that does not exist. # 21.2.0 * [feature] Streaming callable functions are now supported. @@ -235,4 +235,3 @@ updates. optional region to override the default "us-central1". * [feature] New `useFunctionsEmulator` method allows testing against a local instance of the [Cloud Functions Emulator](https://firebase.google.com/docs/functions/local-emulator). - diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js index f26d6615d68..db1b9ab13e6 100644 --- a/firebase-functions/src/androidTest/backend/functions/index.js +++ b/firebase-functions/src/androidTest/backend/functions/index.js @@ -154,6 +154,9 @@ exports.genStream = functionsV2.https.onCall(async (request, response) => { response.sendChunk(chunk); } } + else { + console.log("CLIENT DOES NOT SUPPORT STEAMING"); + } return streamData.join(' '); }); @@ -225,6 +228,8 @@ exports.genStreamLargeData = functionsV2.https.onCall( response.sendChunk(chunk); await sleep(100); } + } else { + console.log("CLIENT DOES NOT SUPPORT STEAMING") } return "Stream Completed"; } diff --git a/firebase-functions/src/androidTest/backend/functions/package.json b/firebase-functions/src/androidTest/backend/functions/package.json index 6c5f9933d8b..53a2aac9d4a 100644 --- a/firebase-functions/src/androidTest/backend/functions/package.json +++ b/firebase-functions/src/androidTest/backend/functions/package.json @@ -2,11 +2,11 @@ "name": "functions", "description": "Cloud Functions for Firebase", "dependencies": { - "firebase-admin": "11.8.0", - "firebase-functions": "4.4.0" + "firebase-admin": "13.2.0", + "firebase-functions": "6.3.2" }, "private": true, "engines": { - "node": "18" + "node": "22" } } diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt index 300385f6a13..f38c39441cd 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -116,11 +116,11 @@ class StreamTests { throwable = e } + assertThat(throwable).isNull() assertThat(messages.map { it.message.data.toString() }) .containsExactly("hello", "world", "this", "is", "cool") assertThat(result).isNotNull() assertThat(result!!.result.data.toString()).isEqualTo("hello world this is cool") - assertThat(throwable).isNull() assertThat(isComplete).isTrue() } @@ -196,6 +196,7 @@ class StreamTests { function.stream(mapOf("data" to "test")).subscribe(subscriber) withTimeout(2000) { delay(500) } + assertThat(subscriber.throwable).isNull() assertThat(subscriber.messages).isEmpty() assertThat(subscriber.result).isNull() } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt index a8ef77c9442..d853dfbbb7b 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/PublisherStream.kt @@ -136,12 +136,20 @@ internal class PublisherStream( MediaType.parse("application/json"), JSONObject(mapOf("data" to serializer.encode(data))).toString() ) - val requestBuilder = - Request.Builder().url(url).post(requestBody).header("Accept", "text/event-stream") - context?.authToken?.let { requestBuilder.header("Authorization", "Bearer $it") } - context?.instanceIdToken?.let { requestBuilder.header("Firebase-Instance-ID-Token", it) } - context?.appCheckToken?.let { requestBuilder.header("X-Firebase-AppCheck", it) } - val request = requestBuilder.build() + val request = + Request.Builder() + .url(url) + .post(requestBody) + .apply { + header("Accept", "text/event-stream") + header("Content-Type", "application/json") + context?.apply { + authToken?.let { header("Authorization", "Bearer $it") } + instanceIdToken?.let { header("Firebase-Instance-ID-Token", it) } + appCheckToken?.let { header("X-Firebase-AppCheck", it) } + } + } + .build() val call = configuredClient.newCall(request) activeCall = call @@ -206,6 +214,9 @@ internal class PublisherStream( eventBuffer.append(dataChunk.trim()).append("\n") } } + if (eventBuffer.isNotEmpty()) { + processEvent(eventBuffer.toString()) + } } catch (e: Exception) { notifyError( FirebaseFunctionsException( @@ -296,9 +307,11 @@ internal class PublisherStream( private fun validateResponse(response: Response) { if (response.isSuccessful) return - val htmlContentType = "text/html; charset=utf-8" val errorMessage: String - if (response.code() == 404 && response.header("Content-Type") == htmlContentType) { + if ( + response.code() == 404 && + MediaType.parse(response.header("Content-Type") ?: "")?.subtype() == "html" + ) { errorMessage = """URL not found. Raw response: ${response.body()?.string()}""".trimMargin() notifyError( FirebaseFunctionsException( @@ -307,6 +320,7 @@ internal class PublisherStream( null ) ) + return } val text = response.body()?.string() ?: "" From 16aed70b43cda0c2ca5a886203e2e126eff85379 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Mar 2025 10:48:34 -0400 Subject: [PATCH 059/162] [DataConnect] Update release configuration (#6798) --- firebase-dataconnect/firebase-dataconnect.gradle.kts | 1 - firebase-dataconnect/gradle.properties | 2 +- .../java/com/google/firebase/gradle/plugins/PublishingPlugin.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 87ffe572916..914985ac18d 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -28,7 +28,6 @@ firebaseLibrary { libraryGroup = "dataconnect" testLab.enabled = false publishJavadoc = false - previewMode = "beta" releaseNotes { name.set("{{data_connect_short}}") versionName.set("data-connect") diff --git a/firebase-dataconnect/gradle.properties b/firebase-dataconnect/gradle.properties index e9c28e87f88..71d387ae30b 100644 --- a/firebase-dataconnect/gradle.properties +++ b/firebase-dataconnect/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.0-beta06 +version=16.0.0 latestReleasedVersion=16.0.0-beta05 diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt index e8384b4579f..666509554a1 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt @@ -346,7 +346,6 @@ abstract class PublishingPlugin : Plugin { "com.google.firebase:firebase-database-connection", "com.google.firebase:firebase-database-connection-license", "com.google.firebase:firebase-database-license", - "com.google.firebase:firebase-dataconnect", "com.google.firebase:firebase-datatransport", "com.google.firebase:firebase-appdistribution-ktx", "com.google.firebase:firebase-appdistribution", @@ -792,6 +791,7 @@ abstract class PublishingPlugin : Plugin { "com.google.firebase:firebase-crashlytics-ktx", "com.google.firebase:firebase-crashlytics-ndk", "com.google.firebase:firebase-database", + "com.google.firebase:firebase-dataconnect", "com.google.firebase:firebase-database-ktx", "com.google.firebase:firebase-dynamic-links", "com.google.firebase:firebase-dynamic-links-ktx", From c1ca0210b83f8d94eea14ee1ffe29a98aac154ca Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Mon, 24 Mar 2025 11:03:35 -0400 Subject: [PATCH 060/162] [DataConnect] Add changelog entry back (#6799) --- firebase-dataconnect/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 02a3f1c836f..cd4f0bf277d 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased - +* [changed] Removed the "beta" suffix from the version of the Firebase Data + Connect Android SDK, thus graduating it from "beta" to "generally available". + ([#6792](https://github.com/firebase/firebase-android-sdk/pull/6792)) # 16.0.0-beta05 * [changed] Changed gRPC proto package to v1 (was v1beta). @@ -73,4 +75,3 @@ ([#6299](https://github.com/firebase/firebase-android-sdk/pull/6299)) * [changed] Added `equals` and `hashCode` methods to `GeneratedConnector`. ([#6177](https://github.com/firebase/firebase-android-sdk/pull/6177)) - From e36a15be3b40246358d2ba76b1b37b7a4887052a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 24 Mar 2025 19:43:09 +0000 Subject: [PATCH 061/162] dataconnect: DataConnectExecutableVersions.json updated with versions 1.8.4, 1.8.5, 1.9.0, 1.9.1, and 1.9.2 (#6803) --- .../plugin/DataConnectExecutableVersions.json | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json index bc2038801fe..2ad055edfa9 100644 --- a/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/plugin/DataConnectExecutableVersions.json @@ -1,5 +1,5 @@ { - "defaultVersion": "1.8.3", + "defaultVersion": "1.9.2", "versions": [ { "version": "1.3.4", @@ -486,6 +486,96 @@ "os": "linux", "size": 25448600, "sha512DigestHex": "6734188ed2dc41fdf9922e152848d46a4bd6a30083c918ac0de5197e1f998f8dc2b4e190c47b02c176f68b93591132c29be142b4b61a36c81aec2358a81864c6" + }, + { + "version": "1.8.4", + "os": "windows", + "size": 26020352, + "sha512DigestHex": "a93277e32a3da54e9b6f9153fa056398567a659d0e5e23422c98bea4b480db6c8d49049135575031bad5e109fb06f82cb65d9131dc8b1ecf3a89039854aacc03" + }, + { + "version": "1.8.4", + "os": "macos", + "size": 25588480, + "sha512DigestHex": "7b8d4e605b6c31b0fa82dab74ac215cbe1745f84c83cb7fc71f7d8e0e697e449d50b91f2bc02a0e20eda870169a6f4ab0d65bfda088801f5245d853fc005e98e" + }, + { + "version": "1.8.4", + "os": "linux", + "size": 25501848, + "sha512DigestHex": "be03f18228074e584d8e4b758ad75d22f71b1f6222c4a3c858f89fd081a138dd27dc03bfb43bd85ac21fb0eba6aae464d429c5f3e166a7d86b9daa0a5f3e8644" + }, + { + "version": "1.8.5", + "os": "windows", + "size": 26031616, + "sha512DigestHex": "14a1f69ee9062bfd460573722f8315781ed12e16734f3d09d635881850e33b159930fee403d5d2bd5ec3644fef3d3d869fe003d3760a9b50223de7676a95502c" + }, + { + "version": "1.8.5", + "os": "macos", + "size": 25600768, + "sha512DigestHex": "ecf07c8ab3295e70c15128d5682269efcd89517dd9d068711a028097efd7bb7995611f7e4ee8312107d9b6e0a82b5565557bac658911f2f6ade8363356007183" + }, + { + "version": "1.8.5", + "os": "linux", + "size": 25514136, + "sha512DigestHex": "628fe32575e6caac56130ae8d286156a15b220d4365bef1c99e71b7cbcd7fa76022cca41e1670816723c9ae24c9db32ee41921075293b7a6500ea58c87e08a60" + }, + { + "version": "1.9.0", + "os": "windows", + "size": 26838016, + "sha512DigestHex": "2680b28d4aec2c401974f0f8cf4110b36974acb52fd7afd8bb23d9d9b619308f66352ed4c1e7b3fc492af29ab1e490b7cee879e9f22eaf7dbf96b4d8fa978b55" + }, + { + "version": "1.9.0", + "os": "macos", + "size": 26395392, + "sha512DigestHex": "39e214d639a747f7af7cdad4aff0ed2af0bb6c3544b3c2daf95f43706039e47a3f0492e8a1c13c352e873f3f9c747af1cb4fb80470007c817bd11431905428e7" + }, + { + "version": "1.9.0", + "os": "linux", + "size": 26308760, + "sha512DigestHex": "02f3fa7c1b98876073c909b259c199ed60c6e1d89cf832d41585cc04a7314a1692cb66709c37fde45be680da6ddc14cd11619d9f80350f0eb180a0de9b8ef2da" + }, + { + "version": "1.9.1", + "os": "windows", + "size": 26846720, + "sha512DigestHex": "ef4014f58df5a9ab6e4c5d1a33a384d93affc7b9bb971a99a2672c05147d0cb64005ecda241a96a37984a9b6657ab900c3b26f2c7a5cfb32a24a2591afc9b94d" + }, + { + "version": "1.9.1", + "os": "macos", + "size": 26403584, + "sha512DigestHex": "ec90bd0c21feb5310f528e80b6415ad028a4f09a2ea99e2a1eca135d27a533ceda8f778c007f06e5ecbc5be0e32a2b13b4d8460ac5ad073e4216e7eb643f0b5d" + }, + { + "version": "1.9.1", + "os": "linux", + "size": 26316952, + "sha512DigestHex": "631cb41c1bf8ab18563180112e9f114d96a525884cf96914b69fdcfd861d32aa852b06b1c675f911148d238a4cd4c3d574ef8f73e66444bf5b8f1199da059e13" + }, + { + "version": "1.9.2", + "os": "windows", + "size": 26846208, + "sha512DigestHex": "1faedad0979fe1228b51f8a3b23f97468e2718cee08dfb65ec6b21e2a3cec99eee060cf9ee6560e80c7a5059437378a7e02f1afa77a8f4e931ef1a3294951fd2" + }, + { + "version": "1.9.2", + "os": "macos", + "size": 26403584, + "sha512DigestHex": "4be6adab666688334879a72519337b851b494d63b7059e71f4c23b443d31f442c50bd69837af3897a9a38ac7aae1aebb8d97d33111e520110dac24c2a7f29a1d" + }, + { + "version": "1.9.2", + "os": "linux", + "size": 26316952, + "sha512DigestHex": "0bd4fcb4bdb66aab502000c19df824fc8df192906e712edd0000192beeb0ba3d29f2a627fc3097735dbab2d9bcbc3715e12fb8afb26e17c2e3e40103357b49ae" } ] } \ No newline at end of file From 75be716089b38a2740731ce4df5108b6c8fe630c Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 24 Mar 2025 19:53:19 +0000 Subject: [PATCH 062/162] dataconnect: generateApiTxtFile.sh added (#6802) --- .../scripts/generateApiTxtFile.sh | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 firebase-dataconnect/scripts/generateApiTxtFile.sh diff --git a/firebase-dataconnect/scripts/generateApiTxtFile.sh b/firebase-dataconnect/scripts/generateApiTxtFile.sh new file mode 100755 index 00000000000..cb572c07140 --- /dev/null +++ b/firebase-dataconnect/scripts/generateApiTxtFile.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +readonly PROJECT_ROOT_DIR="$(dirname "$0")/../.." + +readonly args=( + "${PROJECT_ROOT_DIR}/gradlew" + "-p" + "${PROJECT_ROOT_DIR}" + "--configure-on-demand" + "$@" + ":firebase-dataconnect:generateApiTxtFile" +) + +echo "${args[*]}" +exec "${args[@]}" From ba6997578d951f81a5100244ea71b57ffdc16d05 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Mon, 24 Mar 2025 22:10:34 +0000 Subject: [PATCH 063/162] feat: dataconnect: DataConnectOperationException added to support partial errors (#6794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the `DataConnectOperationException` class, a subclass of the previously-existing `DataConnectException` class. This new `DataConnectOperationException` class is thrown by invocations of `QueryRef.execute()` and `MutationRef.execute()` in the case where a response _is_ received from the backend, but the response indicates that the operation could not be executed to completion, including the case that the client SDK fails to decode the response to a higher-level object. Other kinds of errors, such as networking errors, will still be reported as they were previously. Client code can catch `DataConnectOperationException` and check its `response` property to get details about any errors that occurred and any data that was received. If the data was able to be decoded, despite the errors, then it will be available. Also the "raw" data (a `Map`) property will give access to the raw, undecoded data, if any was sent from the backend. This feature is intended to support "partial errors", where an operation can partially succeed and partially fail, and the client application wants to take some special behavior in the case of a partial success. For example, suppose this database schema and connector definition: ``` type Person @table { name: String! } # Notice how both "inserts" use the same ID; this means that one of them # will necessarily fail because you can't have two rows with the same ID. mutation InsertMultiplePeople($id: UUID!, $name1: String!, $name2: String!) { person1: person_insert(data: { id: $id, name: $name1 }) person2: person_insert(data: { id: $id, name: $name2 }) } ``` Here is some code that handles the partial error that will occur if this mutation were to ever be executed: ```kt import com.google.firebase.dataconnect.DataConnectOperationException import com.google.firebase.dataconnect.DataConnectOperationFailureResponse import com.google.firebase.dataconnect.DataConnectPathSegment import com.myapp.myconnector.InsertTwoFoosWithSameIdMutation.Data suspend fun demo(id: UUID, connector: DemoConnector): Data { val result = connector.insertTwoFoosWithSameId.runCatching { execute(id) } result.onSuccess { println("Weird... inserting _both_ entries with ID $id succeeded 🤷") return@demo it.data } val exception = result.exceptionOrNull()!! if (exception !is DataConnectOperationException) { throw exception } // Print warnings messages about which of "foo1" and "foo2" failed to // be inserted by the query. This information is gleaned from the list of // errors provided in the DataConnectOperationFailureResponse. val response: DataConnectOperationFailureResponse<*> = exception.response val errors = response.errors val error1 = errors.firstOrNull { it.path == listOf(DataConnectPathSegment.Field("person1")) } if (error1 == null) { println("Inserting 1st entry with ID $id succeeded") } else { println("Inserting 1st entry with ID $id failed: ${error1.message}") } val error2 = errors.firstOrNull it.path == listOf(DataConnectPathSegment.Field("person2")) } if (error2 == null) { println("Inserting 2nd entry with ID $id succeeded") } else { println("Inserting 2nd entry with ID $id failed: ${error2.message}") } // If decoding the response was actually successful, then return // the decoded response. val data = response.data as? Data if (data != null) { return data } throw exception } ``` --- firebase-dataconnect/CHANGELOG.md | 5 + firebase-dataconnect/api.txt | 41 ++ .../connector/person/person_ops.gql | 11 + firebase-dataconnect/emulator/emulator.sh | 5 +- ...OperationExecutionErrorsIntegrationTest.kt | 262 +++++++++++++ .../testutil/schemas/PersonSchema.kt | 8 +- .../firebase/dataconnect/DataConnectError.kt | 89 ----- .../DataConnectOperationException.kt | 29 ++ .../DataConnectOperationFailureResponse.kt | 116 ++++++ .../dataconnect/DataConnectPathSegment.kt | 56 +++ .../dataconnect/DataConnectUntypedData.kt | 2 +- .../dataconnect/core/DataConnectGrpcClient.kt | 101 +++-- ...DataConnectOperationFailureResponseImpl.kt | 65 ++++ .../firebase/dataconnect/util/ProtoUtil.kt | 10 +- .../dataconnect/DataConnectErrorUnitTest.kt | 305 --------------- .../DataConnectPathSegmentUnitTest.kt | 226 +++++++++++ .../DataConnectSettingsUnitTest.kt | 3 +- .../dataconnect/PathSegmentFieldUnitTest.kt | 107 ------ .../PathSegmentListIndexUnitTest.kt | 107 ------ .../core/DataConnectGrpcClientUnitTest.kt | 303 +++++++++------ ...ectOperationFailureResponseImplUnitTest.kt | 350 ++++++++++++++++++ .../core/MutationRefImplUnitTest.kt | 4 +- .../testutil/property/arbitrary/arbs.kt | 58 +-- .../DataConnectOperationExceptionTestUtils.kt | 78 ++++ .../testutil/property/arbitrary/arbs.kt | 23 ++ 25 files changed, 1569 insertions(+), 795 deletions(-) create mode 100644 firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt delete mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt delete mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt create mode 100644 firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index cd4f0bf277d..ff10d720603 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -2,6 +2,11 @@ * [changed] Removed the "beta" suffix from the version of the Firebase Data Connect Android SDK, thus graduating it from "beta" to "generally available". ([#6792](https://github.com/firebase/firebase-android-sdk/pull/6792)) +* [changed] DataConnectOperationException added, enabling support for partial + errors; that is, any data that was received and/or was able to be decoded is + now available via the "response" property of the exception thrown when a + query or mutation is executed. + ([#6794](https://github.com/firebase/firebase-android-sdk/pull/6794)) # 16.0.0-beta05 * [changed] Changed gRPC proto package to v1 (was v1beta). diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index 19fb52985f5..d919cc593db 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -42,6 +42,47 @@ package com.google.firebase.dataconnect { ctor public DataConnectException(String message, Throwable? cause = null); } + public class DataConnectOperationException extends com.google.firebase.dataconnect.DataConnectException { + ctor public DataConnectOperationException(String message, Throwable? cause = null, com.google.firebase.dataconnect.DataConnectOperationFailureResponse response); + method public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse getResponse(); + property public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse response; + } + + public interface DataConnectOperationFailureResponse { + method public Data? getData(); + method public java.util.List getErrors(); + method public java.util.Map? getRawData(); + method public String toString(); + property public abstract Data? data; + property public abstract java.util.List errors; + property public abstract java.util.Map? rawData; + } + + public static interface DataConnectOperationFailureResponse.ErrorInfo { + method public boolean equals(Object? other); + method public String getMessage(); + method public java.util.List getPath(); + method public int hashCode(); + method public String toString(); + property public abstract String message; + property public abstract java.util.List path; + } + + public sealed interface DataConnectPathSegment { + } + + @kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.Field implements com.google.firebase.dataconnect.DataConnectPathSegment { + ctor public DataConnectPathSegment.Field(String field); + method public String getField(); + property public final String field; + } + + @kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.ListIndex implements com.google.firebase.dataconnect.DataConnectPathSegment { + ctor public DataConnectPathSegment.ListIndex(int index); + method public int getIndex(); + property public final int index; + } + public final class DataConnectSettings { ctor public DataConnectSettings(String host = "firebasedataconnect.googleapis.com", boolean sslEnabled = true); method public String getHost(); diff --git a/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql b/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql index ffc8281e0fd..37a8a56ba36 100644 --- a/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql +++ b/firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql @@ -88,3 +88,14 @@ query getPersonAuth($id: String!) @auth(level: USER_ANON) { age } } + +query getPersonWithPartialFailure($id: String!) @auth(level: PUBLIC) { + person1: person(id: $id) { name } + person2: person(id: $id) @check(expr: "false", message: "c8azjdwz2x") { name } +} + +mutation createPersonWithPartialFailure($id: String!, $name: String!) @auth(level: PUBLIC) { + person1: person_insert(data: { id: $id, name: $name }) + person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "ecxpjy4qfy") +} + diff --git a/firebase-dataconnect/emulator/emulator.sh b/firebase-dataconnect/emulator/emulator.sh index 68ccf3331a7..c534aaacd12 100755 --- a/firebase-dataconnect/emulator/emulator.sh +++ b/firebase-dataconnect/emulator/emulator.sh @@ -16,9 +16,8 @@ set -euo pipefail -echo "[$0] PID=$$" - -readonly SELF_DIR="$(dirname "$0")" +export FIREBASE_DATACONNECT_POSTGRESQL_STRING='postgresql://postgres:postgres@localhost:5432?sslmode=disable' +echo "[$0] export FIREBASE_DATACONNECT_POSTGRESQL_STRING='$FIREBASE_DATACONNECT_POSTGRESQL_STRING'" readonly FIREBASE_ARGS=( firebase diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt new file mode 100644 index 00000000000..b9844714b01 --- /dev/null +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/OperationExecutionErrorsIntegrationTest.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation +import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery +import com.google.firebase.dataconnect.testutil.shouldSatisfy +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldHaveAtLeastSize +import io.kotest.property.Arb +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.serializer +import org.junit.Test + +class OperationExecutionErrorsIntegrationTest : DataConnectIntegrationTestBase() { + + private val personSchema: PersonSchema by lazy { PersonSchema(dataConnectFactory) } + private val dataConnect: FirebaseDataConnect by lazy { personSchema.dataConnect } + + @Test + fun executeQueryFailsWithNullDataNonEmptyErrors() = runTest { + val queryRef = + dataConnect.query( + operationName = GetPersonQuery.operationName, + variables = Arb.incompatibleVariables().next(rs), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "jwdbzka4k5", + expectedCause = null, + expectedRawData = null, + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNullDataNonEmptyErrors() = runTest { + val mutationRef = + dataConnect.mutation( + operationName = CreatePersonMutation.operationName, + variables = Arb.incompatibleVariables().next(rs), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedCause = null, + expectedRawData = null, + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeQueryFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest { + val id = Arb.alphanumericString().next() + val queryRef = + dataConnect.query( + operationName = GetPersonQuery.operationName, + variables = GetPersonQuery.Variables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = mapOf("person" to null), + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun executeMutationFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = CreatePersonMutation.operationName, + variables = CreatePersonMutation.Variables(id, name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = mapOf("person_insert" to mapOf("id" to id)), + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute() + val queryRef = + dataConnect.query( + operationName = "getPersonWithPartialFailure", + variables = GetPersonWithPartialFailureVariables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "c8azjdwz2x", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null), + expectedData = GetPersonWithPartialFailureData(name), + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = "createPersonWithPartialFailure", + variables = CreatePersonWithPartialFailureVariables(id = id, name = name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "ecxpjy4qfy", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null), + expectedData = CreatePersonWithPartialFailureData(id), + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute() + val queryRef = + dataConnect.query( + operationName = "getPersonWithPartialFailure", + variables = GetPersonWithPartialFailureVariables(id), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { queryRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "c8azjdwz2x", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null), + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Test + fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest { + val id = Arb.alphanumericString().next() + val name = Arb.alphanumericString().next() + val mutationRef = + dataConnect.mutation( + operationName = "createPersonWithPartialFailure", + variables = CreatePersonWithPartialFailureVariables(id = id, name = name), + dataDeserializer = serializer(), + variablesSerializer = serializer(), + optionsBuilder = {}, + ) + + val exception = shouldThrow { mutationRef.execute() } + + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = "ecxpjy4qfy", + expectedCause = null, + expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null), + expectedData = null, + errorsValidator = { it.shouldHaveAtLeastSize(1) }, + ) + } + + @Serializable private data class IncompatibleVariables(val jwdbzka4k5: String) + + @Serializable private data class IncompatibleData(val btzjhbfz7h: String) + + private fun Arb.Companion.incompatibleVariables(string: Arb = Arb.alphanumericString()) = + string.map { IncompatibleVariables(it) } + + @Serializable private data class GetPersonWithPartialFailureVariables(val id: String) + + @Serializable + private data class GetPersonWithPartialFailureData(val person1: Person, val person2: Nothing?) { + constructor(person1Name: String) : this(Person(person1Name), null) + + @Serializable private data class Person(val name: String) + } + + @Serializable + private data class CreatePersonWithPartialFailureVariables(val id: String, val name: String) + + @Serializable + private data class CreatePersonWithPartialFailureData( + val person1: Person, + val person2: Nothing? + ) { + constructor(person1Id: String) : this(Person(person1Id), null) + + @Serializable private data class Person(val id: String) + } +} diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt index 21d9dc9d4dc..a6ed2cfd47a 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt @@ -54,6 +54,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { ) object CreatePersonMutation { + const val operationName = "createPerson" + @Serializable data class Data(val person_insert: PersonKey) { @Serializable data class PersonKey(val id: String) @@ -63,7 +65,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun createPerson(variables: CreatePersonMutation.Variables) = dataConnect.mutation( - operationName = "createPerson", + operationName = CreatePersonMutation.operationName, variables = variables, dataDeserializer = serializer(), variablesSerializer = serializer(), @@ -141,6 +143,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun deletePerson(id: String) = deletePerson(DeletePersonMutation.Variables(id = id)) object GetPersonQuery { + const val operationName = "getPerson" + @Serializable data class Data(val person: Person?) { @Serializable data class Person(val name: String, val age: Int? = null) @@ -151,7 +155,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) { fun getPerson(variables: GetPersonQuery.Variables) = dataConnect.query( - operationName = "getPerson", + operationName = GetPersonQuery.operationName, variables = variables, dataDeserializer = serializer(), variablesSerializer = serializer(), diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt deleted file mode 100644 index 07e87c212a8..00000000000 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectError.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.dataconnect - -import java.util.Objects - -// See https://spec.graphql.org/draft/#sec-Errors -internal class DataConnectError( - val message: String, - val path: List, - val locations: List, -) { - - override fun hashCode(): Int = Objects.hash(message, path, locations) - - override fun equals(other: Any?): Boolean = - (other is DataConnectError) && - other.message == message && - other.path == path && - other.locations == locations - - override fun toString(): String = - StringBuilder() - .also { sb -> - path.forEachIndexed { segmentIndex, segment -> - when (segment) { - is PathSegment.Field -> { - if (segmentIndex != 0) { - sb.append('.') - } - sb.append(segment.field) - } - is PathSegment.ListIndex -> { - sb.append('[') - sb.append(segment.index) - sb.append(']') - } - } - } - - if (locations.isNotEmpty()) { - if (sb.isNotEmpty()) { - sb.append(' ') - } - sb.append("at ") - sb.append(locations.joinToString(", ")) - } - - if (path.isNotEmpty() || locations.isNotEmpty()) { - sb.append(": ") - } - - sb.append(message) - } - .toString() - - sealed interface PathSegment { - @JvmInline - value class Field(val field: String) : PathSegment { - override fun toString(): String = field - } - - @JvmInline - value class ListIndex(val index: Int) : PathSegment { - override fun toString(): String = index.toString() - } - } - - class SourceLocation(val line: Int, val column: Int) { - override fun hashCode(): Int = Objects.hash(line, column) - override fun equals(other: Any?): Boolean = - other is SourceLocation && other.line == line && other.column == column - override fun toString(): String = "$line:$column" - } -} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt new file mode 100644 index 00000000000..cb1ef1b8526 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationException.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +/** + * The exception thrown when an error occurs in the execution of a Firebase Data Connect operation + * (that is, a query or mutation). This exception means that a response was, indeed, received from + * the backend but either the response included one or more errors or the client could not + * successfully process the result (for example, decoding the response data failed). + */ +public open class DataConnectOperationException( + message: String, + cause: Throwable? = null, + public val response: DataConnectOperationFailureResponse<*>, +) : DataConnectException(message, cause) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt new file mode 100644 index 00000000000..386fb8bce9d --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectOperationFailureResponse.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +// Googlers see go/dataconnect:sdk:partial-errors for design details. + +/** The data and errors provided by the backend in the response message. */ +public interface DataConnectOperationFailureResponse { + + /** + * The raw, un-decoded data provided by the backend in the response message. Will be `null` if, + * and only if, the backend explicitly sent null for the data or if the data was not present in + * the response. + * + * Otherwise, the values in the map will be one of the following: + * * `null` + * * [String] + * * [Boolean] + * * [Double] + * * [List] containing any of the types in this list of types + * * [Map] with [String] keys and values of of the types in this list of types + * + * Consider using [toJson] to get a higher-level object. + */ + public val rawData: Map? + + /** + * The list of errors provided by the backend in the response message; may be empty. + * + * See https://spec.graphql.org/draft/#sec-Errors for details. + */ + public val errors: List + + /** + * The successfully-decoded [rawData], if any. + * + * Will be `null` if [rawData] is `null`, or if decoding the [rawData] failed. + */ + public val data: Data? + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at any + * time. Therefore, the only recommended usage of the returned string is debugging and/or logging. + * Namely, parsing the returned string or storing the returned string in non-volatile storage + * should generally be avoided in order to be robust in case that the string representation + * changes. + * + * @return a string representation of this object, which includes the class name and the values of + * all public properties. + */ + override fun toString(): String + + /** + * Information about the error, as provided in the response payload from the backend. + * + * See https://spec.graphql.org/draft/#sec-Errors for details. + */ + public interface ErrorInfo { + /** The error's message. */ + public val message: String + + /** The path of the field in the response data to which this error relates. */ + public val path: List + + /** + * Compares this object with another object for equality. + * + * @param other The object to compare to this for equality. + * @return true if, and only if, the other object is an instance of the same implementation of + * [ErrorInfo] whose public properties compare equal using the `==` operator to the + * corresponding properties of this object. + */ + override fun equals(other: Any?): Boolean + + /** + * Calculates and returns the hash code for this object. + * + * The hash code is _not_ guaranteed to be stable across application restarts. + * + * @return the hash code for this object, that incorporates the values of this object's public + * properties. + */ + override fun hashCode(): Int + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return a string representation of this object, suitable for logging the error indicated by + * this object; it will include the path formatted into a human-readable string (if the path is + * not empty), and the message. + */ + override fun toString(): String + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt new file mode 100644 index 00000000000..3bae99ef78f --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectPathSegment.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +/** The "segment" of a path to a field in the response data. */ +public sealed interface DataConnectPathSegment { + + /** A named field in a path to a field in the response data. */ + @JvmInline + public value class Field(public val field: String) : DataConnectPathSegment { + + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return returns simply [field]. + */ + override fun toString(): String = field + } + + /** An index of a list in a path to a field in the response data. */ + @JvmInline + public value class ListIndex(public val index: Int) : DataConnectPathSegment { + /** + * Returns a string representation of this object, useful for debugging. + * + * The string representation is _not_ guaranteed to be stable and may change without notice at + * any time. Therefore, the only recommended usage of the returned string is debugging and/or + * logging. Namely, parsing the returned string or storing the returned string in non-volatile + * storage should generally be avoided in order to be robust in case that the string + * representation changes. + * + * @return returns simply the string representation of [index]. + */ + override fun toString(): String = index.toString() + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt index 638fffb913e..332cd5251e4 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/DataConnectUntypedData.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.encoding.Decoder internal class DataConnectUntypedData( val data: Map?, - val errors: List + val errors: List ) { override fun equals(other: Any?) = diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt index b2d2270056b..26e9ce49c51 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt @@ -17,15 +17,16 @@ package com.google.firebase.dataconnect.core import com.google.firebase.dataconnect.* -import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toDataConnectError +import com.google.firebase.dataconnect.DataConnectPathSegment +import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toErrorInfoImpl import com.google.firebase.dataconnect.core.LoggerGlobals.warn import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct +import com.google.firebase.dataconnect.util.ProtoUtil.toCompactString import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.ListValue import com.google.protobuf.Struct import com.google.protobuf.Value import google.firebase.dataconnect.proto.GraphqlError -import google.firebase.dataconnect.proto.SourceLocation import google.firebase.dataconnect.proto.executeMutationRequest import google.firebase.dataconnect.proto.executeQueryRequest import io.grpc.Status @@ -52,7 +53,7 @@ internal class DataConnectGrpcClient( data class OperationResult( val data: Struct?, - val errors: List, + val errors: List, ) suspend fun executeQuery( @@ -74,7 +75,7 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toErrorInfoImpl() } ) } @@ -97,7 +98,7 @@ internal class DataConnectGrpcClient( return OperationResult( data = if (response.hasData()) response.data else null, - errors = response.errorsList.map { it.toDataConnectError() } + errors = response.errorsList.map { it.toErrorInfoImpl() } ) } @@ -138,50 +139,72 @@ internal class DataConnectGrpcClient( internal object DataConnectGrpcClientGlobals { private fun ListValue.toPathSegment() = valuesList.map { - when (val kind = it.kindCase) { - Value.KindCase.STRING_VALUE -> DataConnectError.PathSegment.Field(it.stringValue) - Value.KindCase.NUMBER_VALUE -> - DataConnectError.PathSegment.ListIndex(it.numberValue.toInt()) - else -> DataConnectError.PathSegment.Field("invalid PathSegment kind: $kind") + when (it.kindCase) { + Value.KindCase.STRING_VALUE -> DataConnectPathSegment.Field(it.stringValue) + Value.KindCase.NUMBER_VALUE -> DataConnectPathSegment.ListIndex(it.numberValue.toInt()) + // The cases below are expected to never occur; however, implement some logic for them + // to avoid things like throwing exceptions in those cases. + Value.KindCase.NULL_VALUE -> DataConnectPathSegment.Field("null") + Value.KindCase.BOOL_VALUE -> DataConnectPathSegment.Field(it.boolValue.toString()) + Value.KindCase.LIST_VALUE -> DataConnectPathSegment.Field(it.listValue.toCompactString()) + Value.KindCase.STRUCT_VALUE -> + DataConnectPathSegment.Field(it.structValue.toCompactString()) + else -> DataConnectPathSegment.Field(it.toString()) } } - private fun List.toSourceLocations(): List = - buildList { - this@toSourceLocations.forEach { - add(DataConnectError.SourceLocation(line = it.line, column = it.column)) - } - } - - fun GraphqlError.toDataConnectError() = - DataConnectError( + fun GraphqlError.toErrorInfoImpl() = + DataConnectOperationFailureResponseImpl.ErrorInfoImpl( message = message, path = path.toPathSegment(), - this.locationsList.toSourceLocations() ) fun DataConnectGrpcClient.OperationResult.deserialize( deserializer: DeserializationStrategy, serializersModule: SerializersModule?, - ): T = + ): T { if (deserializer === DataConnectUntypedData) { - @Suppress("UNCHECKED_CAST") - DataConnectUntypedData(data?.toMap(), errors) as T - } else if (data === null) { - if (errors.isNotEmpty()) { - throw DataConnectException("operation failed: errors=$errors") - } else { - throw DataConnectException("no data included in result") - } - } else if (errors.isNotEmpty()) { - throw DataConnectException("operation failed: errors=$errors (data=$data)") - } else { - try { - decodeFromStruct(data, deserializer, serializersModule) - } catch (dataConnectException: DataConnectException) { - throw dataConnectException - } catch (throwable: Throwable) { - throw DataConnectException("decoding response data failed: $throwable", throwable) - } + @Suppress("UNCHECKED_CAST") return DataConnectUntypedData(data?.toMap(), errors) as T + } + + val decodedData: Result? = + data?.let { data -> runCatching { decodeFromStruct(data, deserializer, serializersModule) } } + + if (errors.isNotEmpty()) { + throw DataConnectOperationException( + "operation encountered errors during execution: $errors", + response = + DataConnectOperationFailureResponseImpl( + rawData = data?.toMap(), + data = decodedData?.getOrNull(), + errors = errors, + ) + ) + } + + if (decodedData == null) { + throw DataConnectOperationException( + "no data was included in the response from the server", + response = + DataConnectOperationFailureResponseImpl( + rawData = null, + data = null, + errors = emptyList(), + ) + ) } + + return decodedData.getOrElse { exception -> + throw DataConnectOperationException( + "decoding data from the server's response failed: ${exception.message}", + cause = exception, + response = + DataConnectOperationFailureResponseImpl( + rawData = data?.toMap(), + data = null, + errors = emptyList(), + ) + ) + } + } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt new file mode 100644 index 00000000000..85434a64b47 --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImpl.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import com.google.firebase.dataconnect.DataConnectPathSegment +import java.util.Objects + +internal class DataConnectOperationFailureResponseImpl( + override val rawData: Map?, + override val data: Data?, + override val errors: List +) : DataConnectOperationFailureResponse { + + override fun toString(): String = + "DataConnectOperationFailureResponseImpl(rawData=$rawData, data=$data, errors=$errors)" + + internal class ErrorInfoImpl( + override val message: String, + override val path: List, + ) : ErrorInfo { + + override fun equals(other: Any?): Boolean = + other is ErrorInfoImpl && other.message == message && other.path == path + + override fun hashCode(): Int = Objects.hash("ErrorInfoImpl", message, path) + + override fun toString(): String = buildString { + path.forEachIndexed { segmentIndex, segment -> + when (segment) { + is DataConnectPathSegment.Field -> { + if (segmentIndex != 0) { + append('.') + } + append(segment.field) + } + is DataConnectPathSegment.ListIndex -> { + append('[').append(segment.index).append(']') + } + } + } + + if (path.isNotEmpty()) { + append(": ") + } + + append(message) + } + } +} diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt index 0ba6a34b34a..94a3a63a68d 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoUtil.kt @@ -16,7 +16,7 @@ package com.google.firebase.dataconnect.util -import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toDataConnectError +import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toErrorInfoImpl import com.google.firebase.dataconnect.util.ProtoUtil.nullProtoValue import com.google.firebase.dataconnect.util.ProtoUtil.toValueProto import com.google.protobuf.ListValue @@ -136,6 +136,10 @@ internal object ProtoUtil { fun Struct.toCompactString(keySortSelector: ((String) -> String)? = null): String = Value.newBuilder().setStructValue(this).build().toCompactString(keySortSelector) + /** Generates and returns a string similar to [Struct.toString] but more compact. */ + fun ListValue.toCompactString(keySortSelector: ((String) -> String)? = null): String = + Value.newBuilder().setListValue(this).build().toCompactString(keySortSelector) + /** Generates and returns a string similar to [Value.toString] but more compact. */ fun Value.toCompactString(keySortSelector: ((String) -> String)? = null): String { val charArrayWriter = CharArrayWriter() @@ -204,7 +208,7 @@ internal object ProtoUtil { fun ExecuteQueryResponse.toStructProto(): Struct = buildStructProto { if (hasData()) put("data", data) - putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + putList("errors") { errorsList.forEach { add(it.toErrorInfoImpl().toString()) } } } fun ExecuteMutationRequest.toCompactString(): String = toStructProto().toCompactString() @@ -219,7 +223,7 @@ internal object ProtoUtil { fun ExecuteMutationResponse.toStructProto(): Struct = buildStructProto { if (hasData()) put("data", data) - putList("errors") { errorsList.forEach { add(it.toDataConnectError().toString()) } } + putList("errors") { errorsList.forEach { add(it.toErrorInfoImpl().toString()) } } } fun EmulatorInfo.toStructProto(): Struct = buildStructProto { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt deleted file mode 100644 index 204b6f1fc48..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectErrorUnitTest.kt +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError -import com.google.firebase.dataconnect.testutil.property.arbitrary.fieldPathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.listIndexPathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.pathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.sourceLocation -import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText -import io.kotest.assertions.assertSoftly -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeSameInstanceAs -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.Codepoint -import io.kotest.property.arbitrary.az -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.constant -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.list -import io.kotest.property.arbitrary.next -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class DataConnectErrorUnitTest { - - @Test - fun `properties should be the same objects given to the constructor`() = runTest { - val messages = Arb.dataConnect.string() - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - val sourceLocations = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, messages, paths, sourceLocations) { message, path, locations -> - val dataConnectError = DataConnectError(message = message, path = path, locations = locations) - assertSoftly { - dataConnectError.message shouldBeSameInstanceAs message - dataConnectError.path shouldBeSameInstanceAs path - dataConnectError.locations shouldBeSameInstanceAs locations - } - } - } - - @Test - fun `toString() should incorporate the message`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.toString() shouldContainWithNonAbuttingText dataConnectError.message - } - } - - @Test - fun `toString() should incorporate the fields from the path separated by dots`() = runTest { - val paths = Arb.list(Arb.dataConnect.fieldPathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(path = paths)) { dataConnectError -> - val expectedSubstring = dataConnectError.path.joinToString(".") - dataConnectError.toString() shouldContainWithNonAbuttingText expectedSubstring - } - } - - @Test - fun `toString() should incorporate the list indexes from the path surround by square brackets`() = - runTest { - val paths = Arb.list(Arb.dataConnect.listIndexPathSegment(), 1..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(path = paths)) { dataConnectError -> - val expectedSubstring = dataConnectError.path.joinToString(separator = "") { "[$it]" } - dataConnectError.toString() shouldContainWithNonAbuttingText expectedSubstring - } - } - - @Test - fun `toString() should incorporate the fields and list indexes from the path`() { - // Use an example instead of Arb here because using Arb would essentially be re-writing the - // logic that is implemented in DataConnectError.toString(). - val path = - listOf( - PathSegment.Field("foo"), - PathSegment.ListIndex(99), - PathSegment.Field("bar"), - PathSegment.ListIndex(22), - PathSegment.ListIndex(33) - ) - val dataConnectError = Arb.dataConnect.dataConnectError(path = Arb.constant(path)).next() - - dataConnectError.toString() shouldContainWithNonAbuttingText "foo[99].bar[22][33]" - } - - @Test - fun `toString() should incorporate the locations`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - assertSoftly { - dataConnectError.locations.forEach { - dataConnectError.toString() shouldContainWithNonAbuttingText "${it.line}:${it.column}" - } - } - } - } - - @Test - fun `equals() should return true for the exact same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.equals(dataConnectError) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError1 -> - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = List(dataConnectError1.path.size) { dataConnectError1.path[it] }, - locations = List(dataConnectError1.locations.size) { dataConnectError1.locations[it] }, - ) - dataConnectError1.equals(dataConnectError2) shouldBe true - dataConnectError2.equals(dataConnectError1) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - dataConnectError.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.sourceLocation()) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), otherTypes) { - dataConnectError, - other -> - dataConnectError.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false when only message differs`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), Arb.string()) { - dataConnectError1, - newMessage -> - assume(dataConnectError1.message != newMessage) - val dataConnectError2 = - DataConnectError( - message = newMessage, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when message differs only in character case`() = runTest { - val message = Arb.string(1..100, Codepoint.az()) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(message = message)) { dataConnectError - -> - val dataConnectError1 = - DataConnectError( - message = dataConnectError.message.uppercase(), - path = dataConnectError.path, - locations = dataConnectError.locations, - ) - val dataConnectError2 = - DataConnectError( - message = dataConnectError.message.lowercase(), - path = dataConnectError.path, - locations = dataConnectError.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when path differs`() = runTest { - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), paths) { - dataConnectError1, - otherPath -> - assume(dataConnectError1.path != otherPath) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = otherPath, - locations = dataConnectError1.locations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `equals() should return false when locations differ`() = runTest { - val location = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), location) { - dataConnectError1, - otherLocations -> - assume(dataConnectError1.locations != otherLocations) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = otherLocations, - ) - dataConnectError1.equals(dataConnectError2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value each time it is invoked on a given object`() = - runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError -> - val hashCode1 = dataConnectError.hashCode() - dataConnectError.hashCode() shouldBe hashCode1 - dataConnectError.hashCode() shouldBe hashCode1 - } - } - - @Test - fun `hashCode() should return the same value on equal objects`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError()) { dataConnectError1 -> - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if message is different`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), Arb.string()) { - dataConnectError1, - newMessage -> - assume(dataConnectError1.message.hashCode() != newMessage.hashCode()) - val dataConnectError2 = - DataConnectError( - message = newMessage, - path = dataConnectError1.path, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if path is different`() = runTest { - val paths = Arb.list(Arb.dataConnect.pathSegment(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), paths) { dataConnectError1, newPath - -> - assume(dataConnectError1.path.hashCode() != newPath.hashCode()) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = newPath, - locations = dataConnectError1.locations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - @Test - fun `hashCode() should return a different value if locations is different`() = runTest { - val locations = Arb.list(Arb.dataConnect.sourceLocation(), 0..5) - checkAll(propTestConfig, Arb.dataConnect.dataConnectError(), locations) { - dataConnectError1, - newLocations -> - assume(dataConnectError1.locations.hashCode() != newLocations.hashCode()) - val dataConnectError2 = - DataConnectError( - message = dataConnectError1.message, - path = dataConnectError1.path, - locations = newLocations, - ) - dataConnectError1.hashCode() shouldNotBe dataConnectError2.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt new file mode 100644 index 00000000000..d4029102a80 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectPathSegmentUnitTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalKotest::class) +@file:Suppress("ReplaceCallWithBinaryOperator") + +package com.google.firebase.dataconnect + +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.fieldPathSegment as fieldPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.listIndexPathSegment as listIndexPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.string +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + +/** Unit tests for [DataConnectPathSegment.Field] */ +class DataConnectPathSegmentFieldUnitTest { + + @Test + fun `constructor should set field property`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { field -> + val pathSegment = DataConnectPathSegment.Field(field) + pathSegment.field shouldBeSameInstanceAs field + } + } + + @Test + fun `toString() should return a string equal to the field property`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.toString() shouldBeSameInstanceAs pathSegment.field + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.equals(pathSegment) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment1: DataConnectPathSegment.Field -> + val pathSegment2 = DataConnectPathSegment.Field(pathSegment1.field) + pathSegment1.equals(pathSegment2) shouldBe true + pathSegment2.equals(pathSegment1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + pathSegment.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), listIndexPathSegmentArb()) + checkAll(propTestConfig, fieldPathSegmentArb(), otherTypes) { + pathSegment: DataConnectPathSegment.Field, + other -> + pathSegment.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when field differs`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb(), fieldPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.Field, + pathSegment2: DataConnectPathSegment.Field -> + assume(pathSegment1.field != pathSegment2.field) + pathSegment1.equals(pathSegment2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment: DataConnectPathSegment.Field -> + val hashCode1 = pathSegment.hashCode() + pathSegment.hashCode() shouldBe hashCode1 + pathSegment.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb()) { pathSegment1: DataConnectPathSegment.Field -> + val pathSegment2 = DataConnectPathSegment.Field(pathSegment1.field) + pathSegment1.hashCode() shouldBe pathSegment2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if field is different`() = runTest { + checkAll(propTestConfig, fieldPathSegmentArb(), fieldPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.Field, + pathSegment2: DataConnectPathSegment.Field -> + assume(pathSegment1.field.hashCode() != pathSegment2.field.hashCode()) + pathSegment1.hashCode() shouldNotBe pathSegment2.hashCode() + } + } +} + +/** Unit tests for [DataConnectPathSegment.ListIndex] */ +class DataConnectPathSegmentListIndexUnitTest { + + @Test + fun `constructor should set index property`() = runTest { + checkAll(propTestConfig, Arb.int()) { listIndex -> + val pathSegment = DataConnectPathSegment.ListIndex(listIndex) + pathSegment.index shouldBe listIndex + } + } + + @Test + fun `toString() should return a string equal to the index property`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.toString() shouldBe "${pathSegment.index}" + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.equals(pathSegment) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex -> + val pathSegment2 = DataConnectPathSegment.ListIndex(pathSegment1.index) + pathSegment1.equals(pathSegment2) shouldBe true + pathSegment2.equals(pathSegment1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + pathSegment.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), fieldPathSegmentArb()) + checkAll(propTestConfig, listIndexPathSegmentArb(), otherTypes) { + pathSegment: DataConnectPathSegment.ListIndex, + other -> + pathSegment.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when field differs`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb(), listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex, + pathSegment2: DataConnectPathSegment.ListIndex -> + assume(pathSegment1.index != pathSegment2.index) + pathSegment1.equals(pathSegment2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment: DataConnectPathSegment.ListIndex -> + val hashCode1 = pathSegment.hashCode() + pathSegment.hashCode() shouldBe hashCode1 + pathSegment.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex -> + val pathSegment2 = DataConnectPathSegment.ListIndex(pathSegment1.index) + pathSegment1.hashCode() shouldBe pathSegment2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if index is different`() = runTest { + checkAll(propTestConfig, listIndexPathSegmentArb(), listIndexPathSegmentArb()) { + pathSegment1: DataConnectPathSegment.ListIndex, + pathSegment2: DataConnectPathSegment.ListIndex -> + assume(pathSegment1.index.hashCode() != pathSegment2.index.hashCode()) + pathSegment1.hashCode() shouldNotBe pathSegment2.hashCode() + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt index 98c5d441433..48c5a12f878 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/DataConnectSettingsUnitTest.kt @@ -20,7 +20,6 @@ package com.google.firebase.dataconnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.sourceLocation import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText import io.kotest.assertions.assertSoftly import io.kotest.common.ExperimentalKotest @@ -99,7 +98,7 @@ class DataConnectSettingsUnitTest { @Test fun `equals() should return false for a different type`() = runTest { - val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.sourceLocation()) + val otherTypes = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.errorPath()) checkAll(propTestConfig, Arb.dataConnect.dataConnectSettings(), otherTypes) { settings, other -> settings.equals(other) shouldBe false } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt deleted file mode 100644 index 75cf78107b3..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentFieldUnitTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.fieldPathSegment -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class PathSegmentFieldUnitTest { - - @Test - fun `field should equal the value given to the constructor`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.field shouldBe field - } - } - - @Test - fun `toString() should equal the field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.toString() shouldBe field - } - } - - @Test - fun `equals() should return true for the same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment()) { segment -> - segment.equals(segment) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment1 = PathSegment.Field(field) - val segment2 = PathSegment.Field(field) - segment1.equals(segment2) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment()) { segment -> - segment.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val others = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.dataConnectSettings()) - checkAll(propTestConfig, Arb.dataConnect.fieldPathSegment(), others) { segment, other -> - segment.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false for a different field`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string(), Arb.dataConnect.string()) { field1, field2 -> - assume(field1 != field2) - val segment1 = PathSegment.Field(field1) - val segment2 = PathSegment.Field(field2) - segment1.equals(segment2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value as the field's hashCode() method`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.string()) { field -> - val segment = PathSegment.Field(field) - segment.hashCode() shouldBe field.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt deleted file mode 100644 index e8a3046d212..00000000000 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/PathSegmentListIndexUnitTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("ReplaceCallWithBinaryOperator") -@file:OptIn(ExperimentalKotest::class) - -package com.google.firebase.dataconnect - -import com.google.firebase.dataconnect.DataConnectError.PathSegment -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.listIndexPathSegment -import io.kotest.common.ExperimentalKotest -import io.kotest.matchers.shouldBe -import io.kotest.property.Arb -import io.kotest.property.PropTestConfig -import io.kotest.property.arbitrary.choice -import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.string -import io.kotest.property.assume -import io.kotest.property.checkAll -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class PathSegmentListIndexUnitTest { - - @Test - fun `index should equal the value given to the constructor`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.index shouldBe index - } - } - - @Test - fun `toString() should equal the index`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.toString() shouldBe "$index" - } - } - - @Test - fun `equals() should return true for the same instance`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment()) { segment -> - segment.equals(segment) shouldBe true - } - } - - @Test - fun `equals() should return true for an equal field`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment1 = PathSegment.ListIndex(index) - val segment2 = PathSegment.ListIndex(index) - segment1.equals(segment2) shouldBe true - } - } - - @Test - fun `equals() should return false for null`() = runTest { - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment()) { segment -> - segment.equals(null) shouldBe false - } - } - - @Test - fun `equals() should return false for a different type`() = runTest { - val others = Arb.choice(Arb.string(), Arb.int(), Arb.dataConnect.dataConnectSettings()) - checkAll(propTestConfig, Arb.dataConnect.listIndexPathSegment(), others) { segment, other -> - segment.equals(other) shouldBe false - } - } - - @Test - fun `equals() should return false for a different index`() = runTest { - checkAll(propTestConfig, Arb.int(), Arb.int()) { index1, index2 -> - assume(index1 != index2) - val segment1 = PathSegment.ListIndex(index1) - val segment2 = PathSegment.ListIndex(index2) - segment1.equals(segment2) shouldBe false - } - } - - @Test - fun `hashCode() should return the same value as the index's hashCode() method`() = runTest { - checkAll(propTestConfig, Arb.int()) { index -> - val segment = PathSegment.ListIndex(index) - segment.hashCode() shouldBe index.hashCode() - } - } - - private companion object { - val propTestConfig = PropTestConfig(iterations = 20) - } -} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt index 61273ec0d24..5cce39e1c3c 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt @@ -13,27 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalKotest::class) + package com.google.firebase.dataconnect.core -import com.google.firebase.dataconnect.DataConnectError -import com.google.firebase.dataconnect.DataConnectException +import com.google.firebase.dataconnect.DataConnectOperationException +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataConnectUntypedData import com.google.firebase.dataconnect.FirebaseDataConnect import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.deserialize +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule +import com.google.firebase.dataconnect.testutil.RandomSeedTestRule import com.google.firebase.dataconnect.testutil.newMockLogger import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError import com.google.firebase.dataconnect.testutil.property.arbitrary.iterator -import com.google.firebase.dataconnect.testutil.property.arbitrary.operationResult +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors import com.google.firebase.dataconnect.testutil.property.arbitrary.proto import com.google.firebase.dataconnect.testutil.property.arbitrary.struct import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining +import com.google.firebase.dataconnect.testutil.shouldSatisfy import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.ListValue +import com.google.protobuf.Struct import com.google.protobuf.Value import google.firebase.dataconnect.proto.ExecuteMutationRequest import google.firebase.dataconnect.proto.ExecuteMutationResponse @@ -43,50 +49,57 @@ import google.firebase.dataconnect.proto.GraphqlError import google.firebase.dataconnect.proto.SourceLocation import io.grpc.Status import io.grpc.StatusException -import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.maps.shouldContainExactly import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain import io.kotest.matchers.types.shouldBeSameInstanceAs import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig import io.kotest.property.RandomSource import io.kotest.property.arbitrary.Codepoint import io.kotest.property.arbitrary.alphanumeric import io.kotest.property.arbitrary.egyptianHieroglyphs import io.kotest.property.arbitrary.enum -import io.kotest.property.arbitrary.filter import io.kotest.property.arbitrary.int -import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.merge import io.kotest.property.arbitrary.next import io.kotest.property.arbitrary.string -import io.kotest.property.arbs.firstName -import io.kotest.property.arbs.travel.airline +import io.kotest.property.assume import io.kotest.property.checkAll import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify import java.util.concurrent.atomic.AtomicBoolean +import kotlin.reflect.KClass import kotlinx.coroutines.test.runTest import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer import org.junit.Rule import org.junit.Test +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + class DataConnectGrpcClientUnitTest { @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() + @get:Rule val randomSeedTestRule = RandomSeedTestRule() - private val rs = RandomSource.default() + private val rs: RandomSource by randomSeedTestRule.rs private val projectId = Arb.dataConnect.projectId().next(rs) private val connectorConfig = Arb.dataConnect.connectorConfig().next(rs) private val requestId = Arb.dataConnect.requestId().next(rs) @@ -192,7 +205,7 @@ class DataConnectGrpcClientUnitTest { dataConnectGrpcClient.executeQuery(requestId, operationName, variables, callerSdkType) operationResult shouldBe - OperationResult(data = responseData, errors = responseErrors.map { it.dataConnectError }) + OperationResult(data = responseData, errors = responseErrors.map { it.errorInfo }) } @Test @@ -209,7 +222,7 @@ class DataConnectGrpcClientUnitTest { dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) operationResult shouldBe - OperationResult(data = responseData, errors = responseErrors.map { it.dataConnectError }) + OperationResult(data = responseData, errors = responseErrors.map { it.errorInfo }) } @Test @@ -492,7 +505,7 @@ class DataConnectGrpcClientUnitTest { private data class GraphqlErrorInfo( val graphqlError: GraphqlError, - val dataConnectError: DataConnectError, + val errorInfo: ErrorInfoImpl, ) { companion object { private val randomPathComponents = @@ -510,28 +523,24 @@ class DataConnectGrpcClientUnitTest { fun random(rs: RandomSource): GraphqlErrorInfo { - val dataConnectErrorPath = mutableListOf() + val dataConnectErrorPath = mutableListOf() val graphqlErrorPath = ListValue.newBuilder() repeat(6) { if (rs.random.nextFloat() < 0.33f) { val pathComponent = randomInts.next(rs) - dataConnectErrorPath.add(DataConnectError.PathSegment.ListIndex(pathComponent)) + dataConnectErrorPath.add(DataConnectPathSegment.ListIndex(pathComponent)) graphqlErrorPath.addValues(Value.newBuilder().setNumberValue(pathComponent.toDouble())) } else { val pathComponent = randomPathComponents.next(rs) - dataConnectErrorPath.add(DataConnectError.PathSegment.Field(pathComponent)) + dataConnectErrorPath.add(DataConnectPathSegment.Field(pathComponent)) graphqlErrorPath.addValues(Value.newBuilder().setStringValue(pathComponent)) } } - val dataConnectErrorLocations = mutableListOf() val graphqlErrorLocations = mutableListOf() repeat(3) { val line = randomInts.next(rs) val column = randomInts.next(rs) - dataConnectErrorLocations.add( - DataConnectError.SourceLocation(line = line, column = column) - ) graphqlErrorLocations.add( SourceLocation.newBuilder().setLine(line).setColumn(column).build() ) @@ -547,14 +556,13 @@ class DataConnectGrpcClientUnitTest { } .build() - val dataConnectError = - DataConnectError( + val errorInfo = + ErrorInfoImpl( message = message, path = dataConnectErrorPath.toList(), - locations = dataConnectErrorLocations.toList() ) - return GraphqlErrorInfo(graphqlError, dataConnectError) + return GraphqlErrorInfo(graphqlError, errorInfo) } } } @@ -563,69 +571,147 @@ class DataConnectGrpcClientUnitTest { @Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE") class DataConnectGrpcClientOperationResultUnitTest { - private val rs = RandomSource.default() - @Test fun `deserialize() should ignore the module given with DataConnectUntypedData`() { - val errors = listOf(Arb.dataConnect.dataConnectError().next()) - val operationResult = OperationResult(buildStructProto { put("foo", 42.0) }, errors) + val data = buildStructProto { put("foo", 42.0) } + val errors = Arb.dataConnect.operationErrors().next() + val operationResult = OperationResult(data, errors) val result = operationResult.deserialize(DataConnectUntypedData, mockk()) - result shouldBe DataConnectUntypedData(mapOf("foo" to 42.0), errors) + result.shouldHaveDataAndErrors(data, errors) } @Test - fun `deserialize() should treat DataConnectUntypedData specially`() = runTest { - checkAll(iterations = 20, Arb.dataConnect.operationResult()) { operationResult -> + fun `deserialize() with null data should treat DataConnectUntypedData specially`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrors()) { errors -> + val operationResult = OperationResult(null, errors) val result = operationResult.deserialize(DataConnectUntypedData, serializersModule = null) + result.shouldHaveDataAndErrors(null, errors) + } + } - result.asClue { - if (operationResult.data === null) { - it.data.shouldBeNull() - } else { - it.data shouldBe operationResult.data.toMap() - } - it.errors shouldContainExactly operationResult.errors - } + @Test + fun `deserialize() with non-null data should treat DataConnectUntypedData specially`() = runTest { + checkAll(propTestConfig, Arb.proto.struct(), Arb.dataConnect.operationErrors()) { data, errors + -> + val operationResult = OperationResult(data, errors) + val result = operationResult.deserialize(DataConnectUntypedData, serializersModule = null) + result.shouldHaveDataAndErrors(data, errors) } } @Test - fun `deserialize() should throw if one or more errors and data is null`() = runTest { - val arb = - Arb.dataConnect - .operationResult() - .filter { it.errors.isNotEmpty() } - .map { it.copy(data = null) } - checkAll(iterations = 5, arb) { operationResult -> - val exception = - shouldThrow { - operationResult.deserialize(mockk(), serializersModule = null) - } - exception.message shouldContain "${operationResult.errors}" + fun `deserialize() successfully deserializes`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { fooValue -> + val dataStruct = buildStructProto { put("foo", fooValue) } + val operationResult = OperationResult(dataStruct, emptyList()) + + val deserializedData = operationResult.deserialize(serializer(), null) + + deserializedData shouldBe TestData(fooValue) } } @Test - fun `deserialize() should throw if one or more errors and data is _not_ null`() = runTest { - val arb = - Arb.dataConnect.operationResult().filter { it.data !== null && it.errors.isNotEmpty() } - checkAll(iterations = 5, arb) { operationResult -> - val exception = - shouldThrow { + fun `deserialize() should throw if one or more errors and data is null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrors(range = 1..10)) { errors -> + val operationResult = OperationResult(null, errors) + val exception: DataConnectOperationException = + shouldThrow { operationResult.deserialize(mockk(), serializersModule = null) } - exception.message shouldContain "${operationResult.errors}" + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = null, + expectedData = null, + expectedErrors = errors, + ) } } + @Test + fun `deserialize() should throw if one or more errors, data is NOT null, and decoding fails`() = + runTest { + checkAll( + propTestConfig, + Arb.proto.struct(), + Arb.dataConnect.operationErrors(range = 1..10) + ) { dataStruct, errors -> + val operationResult = OperationResult(dataStruct, errors) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(mockk(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = dataStruct, + expectedData = null, + expectedErrors = errors, + ) + } + } + + @Test + fun `deserialize() should throw if one or more errors, data is NOT null, and decoding succeeds`() = + runTest { + checkAll( + propTestConfig, + Arb.dataConnect.string(), + Arb.dataConnect.operationErrors(range = 1..10) + ) { fooValue, errors -> + val dataStruct = buildStructProto { put("foo", fooValue) } + val operationResult = OperationResult(dataStruct, errors) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "operation encountered errors", + expectedMessageSubstringCaseSensitive = errors.toString(), + expectedCause = null, + expectedRawData = dataStruct, + expectedData = TestData(fooValue), + expectedErrors = errors, + ) + } + } + @Test fun `deserialize() should throw if data is null and errors is empty`() { - val operationResult = OperationResult(data = null, errors = emptyList()) - val exception = - shouldThrow { - operationResult.deserialize(mockk(), serializersModule = null) + val operationResult = OperationResult(null, emptyList()) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) } - exception.message shouldContain "no data" + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "no data was included", + expectedCause = null, + expectedRawData = null, + expectedData = null, + expectedErrors = emptyList(), + ) + } + + @Test + fun `deserialize() should throw if decoding fails and error list is empty`() = runTest { + checkAll(propTestConfig, Arb.proto.struct()) { dataStruct -> + assume(!dataStruct.containsFields("foo")) + val operationResult = OperationResult(dataStruct, emptyList()) + val exception: DataConnectOperationException = + shouldThrow { + operationResult.deserialize(serializer(), serializersModule = null) + } + exception.shouldSatisfy( + expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed", + expectedCause = SerializationException::class, + expectedRawData = dataStruct, + expectedData = null, + expectedErrors = emptyList(), + ) + } } @Test @@ -642,51 +728,50 @@ class DataConnectGrpcClientOperationResultUnitTest { slot.captured.serializersModule shouldBeSameInstanceAs serializersModule } - @Test - fun `deserialize() successfully deserializes`() = runTest { - val testData = TestData(Arb.firstName().next().name) - val operationResult = OperationResult(encodeToStruct(testData), errors = emptyList()) - - val deserializedData = operationResult.deserialize(serializer(), null) - - deserializedData shouldBe testData - } - - @Test - fun `deserialize() throws if decoding fails`() = runTest { - val data = Arb.proto.struct().next(rs) - val operationResult = OperationResult(data, errors = emptyList()) - shouldThrow { operationResult.deserialize(serializer(), null) } - } - - @Test - fun `deserialize() re-throws DataConnectException`() = runTest { - val data = encodeToStruct(TestData("fe45zhyd3m")) - val operationResult = OperationResult(data = data, errors = emptyList()) - val deserializer: DeserializationStrategy = spyk(serializer()) - val exception = DataConnectException(message = Arb.airline().next().name) - every { deserializer.deserialize(any()) } throws (exception) - - val thrownException = - shouldThrow { operationResult.deserialize(deserializer, null) } - - thrownException shouldBeSameInstanceAs exception - } - - @Test - fun `deserialize() wraps non-DataConnectException in DataConnectException`() = runTest { - val data = encodeToStruct(TestData("rbmkny6b4r")) - val operationResult = OperationResult(data = data, errors = emptyList()) - val deserializer: DeserializationStrategy = spyk(serializer()) - class MyException : Exception("y3cx44q43q") - val exception = MyException() - every { deserializer.deserialize(any()) } throws (exception) + @Serializable data class TestData(val foo: String) - val thrownException = - shouldThrow { operationResult.deserialize(deserializer, null) } + private companion object { + + fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Struct?, + expectedData: T?, + expectedErrors: List, + ) = + shouldSatisfy( + expectedMessageSubstringCaseInsensitive = expectedMessageSubstringCaseInsensitive, + expectedMessageSubstringCaseSensitive = expectedMessageSubstringCaseSensitive, + expectedCause = expectedCause, + expectedRawData = expectedRawData?.toMap(), + expectedData = expectedData, + expectedErrors = expectedErrors, + ) + + fun DataConnectUntypedData.shouldHaveDataAndErrors( + expectedData: Map, + expectedErrors: List, + ) { + assertSoftly { + withClue("data") { data.shouldNotBeNull().shouldContainExactly(expectedData) } + withClue("errors") { errors shouldContainExactly expectedErrors } + } + } - thrownException.cause shouldBeSameInstanceAs exception + fun DataConnectUntypedData.shouldHaveDataAndErrors( + expectedData: Struct, + expectedErrors: List, + ) = shouldHaveDataAndErrors(expectedData.toMap(), expectedErrors) + + fun DataConnectUntypedData.shouldHaveDataAndErrors( + @Suppress("UNUSED_PARAMETER") expectedData: Nothing?, + expectedErrors: List, + ) { + assertSoftly { + withClue("data") { data.shouldBeNull() } + withClue("errors") { errors shouldContainExactly expectedErrors } + } + } } - - @Serializable data class TestData(val foo: String) } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt new file mode 100644 index 00000000000..994d1b405fa --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectOperationFailureResponseImplUnitTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalKotest::class) +@file:Suppress("ReplaceCallWithBinaryOperator") + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.DataConnectPathSegment +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.errorPath as errorPathArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.fieldPathSegment as fieldPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb.listIndexPathSegment as listIndexPathSegmentArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationData +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrorInfo +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationFailureResponseImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRawData +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldEndWith +import io.kotest.matchers.string.shouldStartWith +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.string +import io.kotest.property.assume +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private val propTestConfig = + PropTestConfig(iterations = 20, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)) + +/** Unit tests for [DataConnectOperationFailureResponseImpl] */ +class DataConnectOperationFailureResponseImplUnitTest { + + @Test + fun `constructor should set properties to the given values`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.operationRawData(), + Arb.dataConnect.operationData(), + Arb.dataConnect.operationErrors() + ) { rawData, data, errors -> + val response = DataConnectOperationFailureResponseImpl(rawData, data, errors) + assertSoftly { + withClue("rawData") { response.rawData shouldBeSameInstanceAs rawData } + withClue("data") { response.data shouldBeSameInstanceAs data } + withClue("errors") { response.errors shouldBeSameInstanceAs errors } + } + } + } + + @Test + fun `toString() should incorporate property values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationFailureResponseImpl()) { + response: DataConnectOperationFailureResponseImpl<*> -> + val toStringResult = response.toString() + assertSoftly { + toStringResult shouldStartWith "DataConnectOperationFailureResponseImpl(" + toStringResult shouldEndWith ")" + toStringResult shouldContainWithNonAbuttingText "rawData=${response.rawData}" + toStringResult shouldContainWithNonAbuttingText "data=${response.data}" + toStringResult shouldContainWithNonAbuttingText "errors=${response.errors}" + } + } + } +} + +/** Unit tests for [DataConnectOperationFailureResponseImpl.ErrorInfoImpl] */ +class DataConnectOperationFailureResponseImplErrorInfoImplUnitTest { + + @Test + fun `constructor should set properties to the given values`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), errorPathArb()) { message, path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.message shouldBeSameInstanceAs message + errorInfo.path shouldBeSameInstanceAs path + } + } + + @Test + fun `toString() should return an empty string if both message and path are empty`() { + val errorInfo = ErrorInfoImpl("", emptyList()) + errorInfo.toString() shouldBe "" + } + + @Test + fun `toString() should return the message if message is non-empty and path is empty`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string()) { message -> + val errorInfo = ErrorInfoImpl(message, emptyList()) + errorInfo.toString() shouldBe message + } + } + + @Test + fun `toString() should not do anything different with an empty message`() = runTest { + checkAll(propTestConfig, errorPathArb()) { path -> + assume(path.isNotEmpty()) + val errorInfo = ErrorInfoImpl("", path) + val errorInfoToStringResult = errorInfo.toString() + errorInfoToStringResult shouldEndWith ": " + path.forEachIndexed { index, pathSegment -> + withClue("path[$index]") { + errorInfoToStringResult shouldContainWithNonAbuttingText pathSegment.toString() + } + } + } + } + + @Test + fun `toString() should print field path segments separated by dots`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), Arb.list(fieldPathSegmentArb(), 1..10)) { + message, + path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe path.joinToString(".") + ": $message" + } + } + + @Test + fun `toString() should print list index path segments separated by dots`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.string(), + Arb.list(listIndexPathSegmentArb(), 1..10) + ) { message, path -> + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe path.joinToString("") { "[${it.index}]" } + ": $message" + } + } + + @Test + fun `toString() for path is field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe "${segments.field1.field}[${segments.listIndex1}]: $message" + } + } + + @Test + fun `toString() for path is listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.listIndex1, segments.field1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe "[${segments.listIndex1}].${segments.field1.field}: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.field2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}].${segments.field2.field}: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.listIndex2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}][${segments.listIndex2}]: $message" + } + } + + @Test + fun `toString() for path is field, field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.field2, segments.listIndex1) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}.${segments.field2.field}[${segments.listIndex1}]: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, field, listIndex`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.field2, segments.listIndex2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}].${segments.field2.field}[${segments.listIndex2}]: $message" + } + } + + @Test + fun `toString() for path is field, listIndex, listIndex, field`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.string(), MyArb.samplePathSegments()) { + message, + segments -> + val path = listOf(segments.field1, segments.listIndex1, segments.listIndex2, segments.field2) + val errorInfo = ErrorInfoImpl(message, path) + errorInfo.toString() shouldBe + "${segments.field1.field}[${segments.listIndex1}][${segments.listIndex2}].${segments.field2.field}: $message" + } + } + + @Test + fun `equals() should return true for the exact same instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + errorInfo.equals(errorInfo) shouldBe true + } + } + + @Test + fun `equals() should return true for an equal instance`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo1: ErrorInfoImpl -> + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe true + errorInfo2.equals(errorInfo1) shouldBe true + } + } + + @Test + fun `equals() should return false for null`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + errorInfo.equals(null) shouldBe false + } + } + + @Test + fun `equals() should return false for a different type`() = runTest { + val otherTypes = Arb.choice(Arb.string(), Arb.int(), listIndexPathSegmentArb()) + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), otherTypes) { + errorInfo: ErrorInfoImpl, + other -> + errorInfo.equals(other) shouldBe false + } + } + + @Test + fun `equals() should return false when message differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), Arb.dataConnect.string()) { + errorInfo1: ErrorInfoImpl, + otherMessage: String -> + assume(errorInfo1.message != otherMessage) + val errorInfo2 = ErrorInfoImpl(otherMessage, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `equals() should return false when path differs`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), errorPathArb()) { + errorInfo1: ErrorInfoImpl, + otherPath: List -> + assume(errorInfo1.path != otherPath) + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, otherPath) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `hashCode() should return the same value each time it is invoked on a given object`() = + runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo: ErrorInfoImpl -> + val hashCode1 = errorInfo.hashCode() + errorInfo.hashCode() shouldBe hashCode1 + errorInfo.hashCode() shouldBe hashCode1 + } + } + + @Test + fun `hashCode() should return the same value on equal objects`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo()) { errorInfo1: ErrorInfoImpl -> + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, errorInfo1.path) + errorInfo1.hashCode() shouldBe errorInfo2.hashCode() + } + } + + @Test + fun `hashCode() should return a different value if message is different`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), Arb.dataConnect.string()) { + errorInfo1: ErrorInfoImpl, + otherMessage: String -> + assume(errorInfo1.message.hashCode() != otherMessage.hashCode()) + val errorInfo2 = ErrorInfoImpl(otherMessage, errorInfo1.path) + errorInfo1.equals(errorInfo2) shouldBe false + } + } + + @Test + fun `hashCode() should return a different value if path is different`() = runTest { + checkAll(propTestConfig, Arb.dataConnect.operationErrorInfo(), errorPathArb()) { + errorInfo1: ErrorInfoImpl, + otherPath: List -> + assume(errorInfo1.path.hashCode() != otherPath.hashCode()) + val errorInfo2 = ErrorInfoImpl(errorInfo1.message, otherPath) + errorInfo1.equals(errorInfo2) shouldBe false + } + } +} + +private object MyArb { + + fun samplePathSegments( + field: Arb = fieldPathSegmentArb(), + listIndex: Arb = listIndexPathSegmentArb(), + ): Arb = + Arb.bind(field, field, listIndex, listIndex) { field1, field2, listIndex1, listIndex2 -> + SamplePathSegments(field1, field2, listIndex1, listIndex2) + } + + data class SamplePathSegments( + val field1: DataConnectPathSegment.Field, + val field2: DataConnectPathSegment.Field, + val listIndex1: DataConnectPathSegment.ListIndex, + val listIndex2: DataConnectPathSegment.ListIndex, + ) +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt index f61a824630e..bf200e4caad 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt @@ -26,9 +26,9 @@ import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResul import com.google.firebase.dataconnect.testutil.property.arbitrary.DataConnectArb import com.google.firebase.dataconnect.testutil.property.arbitrary.OperationRefConstructorArguments import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect -import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectError import com.google.firebase.dataconnect.testutil.property.arbitrary.mock import com.google.firebase.dataconnect.testutil.property.arbitrary.mutationRefImpl +import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRefConstructorArguments import com.google.firebase.dataconnect.testutil.property.arbitrary.operationRefImpl import com.google.firebase.dataconnect.testutil.property.arbitrary.queryRefImpl @@ -181,7 +181,7 @@ class MutationRefImplUnitTest { @Test fun `execute() handles DataConnectUntypedVariables and DataConnectUntypedData`() = runTest { val variables = DataConnectUntypedVariables("foo" to 42.0) - val errors = listOf(Arb.dataConnect.dataConnectError().next()) + val errors = Arb.dataConnect.operationErrors().next() val data = DataConnectUntypedData(mapOf("bar" to 24.0), errors) val variablesSlot: CapturingSlot = slot() val operationResult = OperationResult(buildStructProto { put("bar", 24.0) }, errors) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index 44c2a5a4720..89b2c89bb0f 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -18,20 +18,22 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary -import com.google.firebase.dataconnect.DataConnectError -import com.google.firebase.dataconnect.DataConnectError.PathSegment +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType import com.google.firebase.dataconnect.OperationRef import com.google.firebase.dataconnect.core.DataConnectAppCheck import com.google.firebase.dataconnect.core.DataConnectAuth import com.google.firebase.dataconnect.core.DataConnectGrpcClient import com.google.firebase.dataconnect.core.DataConnectGrpcMetadata +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl +import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl import com.google.firebase.dataconnect.core.FirebaseDataConnectImpl import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal import com.google.firebase.dataconnect.core.MutationRefImpl import com.google.firebase.dataconnect.core.OperationRefImpl import com.google.firebase.dataconnect.core.QueryRefImpl import com.google.firebase.dataconnect.testutil.StubOperationRefImpl +import com.google.firebase.dataconnect.util.ProtoUtil.toMap import com.google.protobuf.Struct import io.kotest.assertions.assertSoftly import io.kotest.assertions.withClue @@ -40,11 +42,12 @@ import io.kotest.property.Arb import io.kotest.property.arbitrary.Codepoint import io.kotest.property.arbitrary.alphanumeric import io.kotest.property.arbitrary.arbitrary -import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.bind import io.kotest.property.arbitrary.constant import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string import io.mockk.mockk @@ -75,36 +78,39 @@ internal fun DataConnectArb.dataConnectGrpcMetadata( ) } -internal fun DataConnectArb.fieldPathSegment( - string: Arb = string() -): Arb = arbitrary { PathSegment.Field(string.bind()) } +internal fun DataConnectArb.operationErrorInfo( + message: Arb = string(), + path: Arb> = errorPath(), +): Arb = + Arb.bind(message, path) { message0, path0 -> ErrorInfoImpl(message0, path0) } -internal fun DataConnectArb.listIndexPathSegment( - int: Arb = Arb.int() -): Arb = arbitrary { PathSegment.ListIndex(int.bind()) } +internal fun DataConnectArb.operationRawData(): Arb?> = + Arb.proto.struct().map { it.toMap() }.orNull(nullProbability = 0.33) -internal fun DataConnectArb.pathSegment(): Arb = - Arb.choice(fieldPathSegment(), listIndexPathSegment()) +internal data class SampleOperationData(val value: String) -internal fun DataConnectArb.sourceLocation( - line: Arb = Arb.int(), - column: Arb = Arb.int() -): Arb = arbitrary { - DataConnectError.SourceLocation(line = line.bind(), column = column.bind()) -} +internal fun DataConnectArb.operationData(): Arb = + string().map { SampleOperationData(it) }.orNull(nullProbability = 0.33) -internal fun DataConnectArb.dataConnectError( - message: Arb = string(), - path: Arb> = Arb.list(pathSegment(), 0..5), - locations: Arb> = Arb.list(sourceLocation(), 0..5) -): Arb = arbitrary { - DataConnectError(message = message.bind(), path = path.bind(), locations = locations.bind()) -} +internal fun DataConnectArb.operationErrors( + errorInfoImpl: Arb = operationErrorInfo(), + range: IntRange = 0..10, +): Arb> = Arb.list(errorInfoImpl, range) + +internal fun DataConnectArb.operationFailureResponseImpl( + rawData: Arb?> = operationRawData(), + data: Arb = operationData(), + errors: Arb> = operationErrors(), +): Arb> = + Arb.bind(rawData, data, errors) { rawData0, data0, errors0 -> + DataConnectOperationFailureResponseImpl(rawData0, data0, errors0) + } internal fun DataConnectArb.operationResult( data: Arb = Arb.proto.struct().orNull(nullProbability = 0.2), - errors: Arb> = Arb.list(dataConnectError(), 0..3), -) = arbitrary { DataConnectGrpcClient.OperationResult(data.bind(), errors.bind()) } + errors: Arb> = operationErrors(), +) = + Arb.bind(data, errors) { data0, errors0 -> DataConnectGrpcClient.OperationResult(data0, errors0) } internal fun DataConnectArb.queryRefImpl( variables: Arb, diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt new file mode 100644 index 00000000000..ac1ec9de005 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/DataConnectOperationExceptionTestUtils.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.DataConnectOperationException +import com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import kotlin.reflect.KClass + +fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Map?, + expectedData: T?, + expectedErrors: List, +): Unit = + shouldSatisfy( + expectedMessageSubstringCaseInsensitive = expectedMessageSubstringCaseInsensitive, + expectedMessageSubstringCaseSensitive = expectedMessageSubstringCaseSensitive, + expectedCause = expectedCause, + expectedRawData = expectedRawData, + expectedData = expectedData, + errorsValidator = { it.shouldContainExactly(expectedErrors) }, + ) + +fun DataConnectOperationException.shouldSatisfy( + expectedMessageSubstringCaseInsensitive: String, + expectedMessageSubstringCaseSensitive: String? = null, + expectedCause: KClass<*>?, + expectedRawData: Map?, + expectedData: T?, + errorsValidator: (List) -> Unit, +): Unit { + assertSoftly { + withClue("exception.message") { + message shouldContainWithNonAbuttingTextIgnoringCase expectedMessageSubstringCaseInsensitive + if (expectedMessageSubstringCaseSensitive != null) { + message shouldContainWithNonAbuttingText expectedMessageSubstringCaseSensitive + } + } + withClue("exception.cause") { + if (expectedCause == null) { + cause.shouldBeNull() + } else { + val cause = cause.shouldNotBeNull() + if (!expectedCause.isInstance(cause)) { + io.kotest.assertions.fail( + "cause was an instance of ${cause::class.qualifiedName}, " + + "but expected it to be an instance of ${expectedCause.qualifiedName}" + ) + } + } + } + withClue("exception.response.rawData") { response.rawData shouldBe expectedRawData } + withClue("exception.response.data") { response.data shouldBe expectedData } + withClue("exception.response.errors") { errorsValidator(response.errors) } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index 17d63fc7ebf..4a3f89a7ba8 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -19,6 +19,7 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import com.google.firebase.dataconnect.ConnectorConfig +import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataConnectSettings import io.kotest.property.Arb import io.kotest.property.arbitrary.Codepoint @@ -27,11 +28,15 @@ import io.kotest.property.arbitrary.arabic import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.ascii import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.choose import io.kotest.property.arbitrary.cyrillic import io.kotest.property.arbitrary.double import io.kotest.property.arbitrary.egyptianHieroglyphs import io.kotest.property.arbitrary.filterNot import io.kotest.property.arbitrary.hex +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.merge import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string @@ -132,6 +137,24 @@ object DataConnectArb { fun serializersModule(): Arb = arbitrary { mockk() }.orNull(nullProbability = 0.333) + + fun fieldPathSegment(string: Arb = string()): Arb = + string.map { DataConnectPathSegment.Field(it) } + + fun listIndexPathSegment(int: Arb = Arb.int()): Arb = + int.map { DataConnectPathSegment.ListIndex(it) } + + fun pathSegment( + field: Arb = fieldPathSegment(), + fieldWeight: Int = 1, + listIndex: Arb = listIndexPathSegment(), + listIndexWeight: Int = 1, + ): Arb = Arb.choose(fieldWeight to field, listIndexWeight to listIndex) + + fun errorPath( + pathSegment: Arb = pathSegment(), + range: IntRange = 0..10, + ): Arb> = Arb.list(pathSegment, range) } val Arb.Companion.dataConnect: DataConnectArb From baa335cb1ca8cf95ce924c1030268a907be00319 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:20:06 -0500 Subject: [PATCH 064/162] Add fix for API info task (#6808) --- ci/fireci/fireciplugins/api_information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/fireci/fireciplugins/api_information.py b/ci/fireci/fireciplugins/api_information.py index d10b6866797..05e4966d47c 100644 --- a/ci/fireci/fireciplugins/api_information.py +++ b/ci/fireci/fireciplugins/api_information.py @@ -37,9 +37,9 @@ def api_information(auth_token, repo_name, issue_number): with open(os.path.join(dir_suffix, filename), 'r') as f: outputlines = f.readlines() for line in outputlines: - if 'error' in line: + if 'error:' in line: formatted_output_lines.append(line[line.find('error:'):]) - elif 'warning' in line: + elif 'warning:' in line: formatted_output_lines.append(line[line.find('warning:'):]) if formatted_output_lines: From 1cf65b9d2a473399440df24fab4cda5dfa296468 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Tue, 25 Mar 2025 13:35:43 -0400 Subject: [PATCH 065/162] [VertexAI] Bump timeout for error test (#6807) The test `genStreamError_receivesError` is flaky due to timeout issues. Bumping timeout to 10s. --- .../java/com/google/firebase/functions/StreamTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt index f38c39441cd..8ef45478fff 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -128,14 +128,14 @@ class StreamTests { fun genStreamError_receivesError() = runBlocking { val input = mapOf("data" to "test error") val function = - functions.getHttpsCallable("genStreamError").withTimeout(2000, TimeUnit.MILLISECONDS) + functions.getHttpsCallable("genStreamError").withTimeout(10_000, TimeUnit.MILLISECONDS) val subscriber = StreamSubscriber() function.stream(input).subscribe(subscriber) - withTimeout(2000) { + withTimeout(10_000) { while (subscriber.throwable == null) { - delay(100) + delay(1_000) } } From 564734a4e7152ca1e8743f7f05d53ee0f2679bdc Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 25 Mar 2025 14:14:27 -0400 Subject: [PATCH 066/162] [Vertex AI] Return `ImagenInlineImage.data` as binary (#6800) Co-authored-by: Rodrigo Lazo --- firebase-vertexai/CHANGELOG.md | 7 ++++++- .../firebase/vertexai/type/ImagenGenerationResponse.kt | 3 ++- .../google/firebase/vertexai/type/ImagenInlineImage.kt | 9 ++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index 62db530d71f..e922c2d9f65 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -1,7 +1,12 @@ # Unreleased * [changed] Added new exception type for quota exceeded scenarios. * [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. - +* [changed] **Breaking Change**: `ImagenInlineImage.data` now returns the raw + image bytes (in JPEG or PNG format, as specified in + `ImagenInlineImage.mimeType`) instead of Base64-encoded data. (#6800) + * **Action Required:** Remove any Base64 decoding from your + `ImagenInlineImage.data` usage. + * The `asBitmap()` helper method is unaffected and requires no code changes. # 16.2.0 * [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`. diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt index dfc011b58f9..454f526bbee 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGenerationResponse.kt @@ -16,6 +16,7 @@ package com.google.firebase.vertexai.type +import android.util.Base64 import com.google.firebase.vertexai.ImagenModel import kotlinx.serialization.Serializable @@ -53,7 +54,7 @@ internal constructor(public val images: List, public val filteredReason: Stri val raiFilteredReason: String? = null, ) { internal fun toPublicInline() = - ImagenInlineImage(bytesBase64Encoded!!.toByteArray(), mimeType!!) + ImagenInlineImage(Base64.decode(bytesBase64Encoded!!, Base64.NO_WRAP), mimeType!!) internal fun toPublicGCS() = ImagenGCSImage(gcsUri!!, mimeType!!) } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt index 03e93abf8e7..1004f0a57ac 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenInlineImage.kt @@ -18,13 +18,13 @@ package com.google.firebase.vertexai.type import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.util.Base64 /** - * Represents an Imagen-generated image that is contained inline + * Represents an Imagen-generated image that is returned as inline data. * - * @param data Contains the raw bytes of the image - * @param mimeType Contains the MIME type of the image (for example, `"image/png"`) + * @property data The raw image bytes in JPEG or PNG format, as specified by [mimeType]. + * @property mimeType The IANA standard MIME type of the image data; either `"image/png"` or + * `"image/jpeg"`; to request a different format, see [ImagenGenerationConfig.imageFormat]. */ @PublicPreviewAPI public class ImagenInlineImage @@ -34,7 +34,6 @@ internal constructor(public val data: ByteArray, public val mimeType: String) { * Returns the image as an Android OS native [Bitmap] so that it can be saved or sent to the UI. */ public fun asBitmap(): Bitmap { - val data = Base64.decode(data, Base64.NO_WRAP) return BitmapFactory.decodeByteArray(data, 0, data.size) } } From 8a72ed5841a203e48b6c1d771f52cc71110834cd Mon Sep 17 00:00:00 2001 From: David Motsonashvili Date: Tue, 25 Mar 2025 18:20:10 +0000 Subject: [PATCH 067/162] Add ML monitoring info to GenAI requests (#6804) Co-authored-by: David Motsonashvili --- .../firebase-vertexai.gradle.kts | 1 + .../firebase/vertexai/FirebaseVertexAI.kt | 2 + .../firebase/vertexai/GenerativeModel.kt | 3 + .../google/firebase/vertexai/ImagenModel.kt | 3 + .../firebase/vertexai/common/APIController.kt | 31 +++++++- .../vertexai/GenerativeModelTesting.kt | 18 +++++ .../vertexai/common/APIControllerTests.kt | 74 +++++++++++++++++++ .../firebase/vertexai/common/util/tests.kt | 10 +++ .../google/firebase/vertexai/util/tests.kt | 10 +++ 9 files changed, 151 insertions(+), 1 deletion(-) diff --git a/firebase-vertexai/firebase-vertexai.gradle.kts b/firebase-vertexai/firebase-vertexai.gradle.kts index b43909559e0..08942b6bedf 100644 --- a/firebase-vertexai/firebase-vertexai.gradle.kts +++ b/firebase-vertexai/firebase-vertexai.gradle.kts @@ -115,6 +115,7 @@ dependencies { testImplementation(libs.kotlin.coroutines.test) testImplementation(libs.robolectric) testImplementation(libs.truth) + testImplementation(libs.mockito.core) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.test.junit) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt index b89e5671992..1790ec0c300 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt @@ -71,6 +71,7 @@ internal constructor( return GenerativeModel( "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", firebaseApp.options.apiKey, + firebaseApp, generationConfig, safetySettings, tools, @@ -105,6 +106,7 @@ internal constructor( return ImagenModel( "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", firebaseApp.options.apiKey, + firebaseApp, generationConfig, safetySettings, requestOptions, diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt index 12d89ab5b59..3520aff2238 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/GenerativeModel.kt @@ -17,6 +17,7 @@ package com.google.firebase.vertexai import android.graphics.Bitmap +import com.google.firebase.FirebaseApp import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.vertexai.common.APIController @@ -59,6 +60,7 @@ internal constructor( internal constructor( modelName: String, apiKey: String, + firebaseApp: FirebaseApp, generationConfig: GenerationConfig? = null, safetySettings: List? = null, tools: List? = null, @@ -79,6 +81,7 @@ internal constructor( modelName, requestOptions, "gl-kotlin/${KotlinVersion.CURRENT} fire/${BuildConfig.VERSION_NAME}", + firebaseApp, AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), ), ) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt index 583ef24bcc4..fa33ee6e327 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/ImagenModel.kt @@ -16,6 +16,7 @@ package com.google.firebase.vertexai +import com.google.firebase.FirebaseApp import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.vertexai.common.APIController @@ -46,6 +47,7 @@ internal constructor( internal constructor( modelName: String, apiKey: String, + firebaseApp: FirebaseApp, generationConfig: ImagenGenerationConfig? = null, safetySettings: ImagenSafetySettings? = null, requestOptions: RequestOptions = RequestOptions(), @@ -60,6 +62,7 @@ internal constructor( modelName, requestOptions, "gl-kotlin/${KotlinVersion.CURRENT} fire/${BuildConfig.VERSION_NAME}", + firebaseApp, AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), ), ) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt index f8bfe0bc24f..c67e21ccf23 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt @@ -18,6 +18,7 @@ package com.google.firebase.vertexai.common import android.util.Log import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp import com.google.firebase.options import com.google.firebase.vertexai.common.util.decodeToFlow import com.google.firebase.vertexai.common.util.fullModelName @@ -91,6 +92,9 @@ internal constructor( private val requestOptions: RequestOptions, httpEngine: HttpClientEngine, private val apiClient: String, + private val firebaseApp: FirebaseApp, + private val appVersion: Int = 0, + private val googleAppId: String, private val headerProvider: HeaderProvider?, ) { @@ -99,8 +103,19 @@ internal constructor( model: String, requestOptions: RequestOptions, apiClient: String, + firebaseApp: FirebaseApp, headerProvider: HeaderProvider? = null, - ) : this(key, model, requestOptions, OkHttp.create(), apiClient, headerProvider) + ) : this( + key, + model, + requestOptions, + OkHttp.create(), + apiClient, + firebaseApp, + getVersionNumber(firebaseApp), + firebaseApp.options.applicationId, + headerProvider + ) private val model = fullModelName(model) @@ -175,6 +190,10 @@ internal constructor( contentType(ContentType.Application.Json) header("x-goog-api-key", key) header("x-goog-api-client", apiClient) + if (firebaseApp.isDataCollectionDefaultEnabled) { + header("X-Firebase-AppId", googleAppId) + header("X-Firebase-AppVersion", appVersion) + } } private suspend fun HttpRequestBuilder.applyHeaderProvider() { @@ -240,6 +259,16 @@ internal constructor( companion object { private val TAG = APIController::class.java.simpleName + + private fun getVersionNumber(app: FirebaseApp): Int { + try { + val context = app.applicationContext + return context.packageManager.getPackageInfo(context.packageName, 0).versionCode + } catch (e: Exception) { + Log.d(TAG, "Error while getting app version: ${e.message}") + return 0 + } + } } } diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt index d4c2ad37926..e66918ad52f 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/GenerativeModelTesting.kt @@ -16,6 +16,7 @@ package com.google.firebase.vertexai +import com.google.firebase.FirebaseApp import com.google.firebase.vertexai.common.APIController import com.google.firebase.vertexai.common.JSON import com.google.firebase.vertexai.common.util.doBlocking @@ -42,10 +43,21 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString +import org.junit.Before import org.junit.Test +import org.mockito.Mockito internal class GenerativeModelTesting { private val TEST_CLIENT_ID = "test" + private val TEST_APP_ID = "1:android:12345" + private val TEST_VERSION = 1 + + private var mockFirebaseApp: FirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } @Test fun `system calling in request`() = doBlocking { @@ -64,6 +76,9 @@ internal class GenerativeModelTesting { RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -109,6 +124,9 @@ internal class GenerativeModelTesting { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt index 29b52b81d1b..0d668849156 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/APIControllerTests.kt @@ -16,6 +16,7 @@ package com.google.firebase.vertexai.common +import com.google.firebase.FirebaseApp import com.google.firebase.vertexai.BuildConfig import com.google.firebase.vertexai.common.util.commonTest import com.google.firebase.vertexai.common.util.createResponses @@ -49,12 +50,18 @@ import kotlinx.coroutines.withTimeout import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonObject +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.mockito.Mockito private val TEST_CLIENT_ID = "genai-android/test" +private val TEST_APP_ID = "1:android:12345" + +private val TEST_VERSION = 1 + internal class APIControllerTests { private val testTimeout = 5.seconds @@ -87,6 +94,14 @@ internal class APIControllerTests { @OptIn(ExperimentalSerializationApi::class) internal class RequestFormatTests { + + private val mockFirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } + @Test fun `using default endpoint`() = doBlocking { val channel = ByteChannel(autoFlush = true) @@ -101,6 +116,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, "genai-android/${BuildConfig.VERSION_NAME}", + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -128,6 +146,9 @@ internal class RequestFormatTests { RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -155,6 +176,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -163,6 +187,35 @@ internal class RequestFormatTests { mockEngine.requestHistory.first().headers["x-goog-api-client"] shouldBe TEST_CLIENT_ID } + @Test + fun `ml monitoring header is set correctly if data collection is enabled`() = doBlocking { + val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) + val mockEngine = MockEngine { + respond(response, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(true) + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-1.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { controller.countTokens(textCountTokenRequest("cats")) } + + mockEngine.requestHistory.first().headers["X-Firebase-AppId"] shouldBe TEST_APP_ID + mockEngine.requestHistory.first().headers["X-Firebase-AppVersion"] shouldBe + TEST_VERSION.toString() + } + @Test fun `ToolConfig serialization contains correct keys`() = doBlocking { val channel = ByteChannel(autoFlush = true) @@ -178,6 +231,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -229,6 +285,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, testHeaderProvider, ) @@ -263,6 +322,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, testHeaderProvider, ) @@ -286,6 +348,9 @@ internal class RequestFormatTests { RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) @@ -309,6 +374,12 @@ internal class RequestFormatTests { @RunWith(Parameterized::class) internal class ModelNamingTests(private val modelName: String, private val actualName: String) { + private val mockFirebaseApp = Mockito.mock() + + @Before + fun setup() { + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + } @Test fun `request should include right model name`() = doBlocking { @@ -324,6 +395,9 @@ internal class ModelNamingTests(private val modelName: String, private val actua RequestOptions(), mockEngine, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt index 855c8aa4a8b..320cf381467 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/common/util/tests.kt @@ -18,6 +18,7 @@ package com.google.firebase.vertexai.common.util +import com.google.firebase.FirebaseApp import com.google.firebase.vertexai.common.APIController import com.google.firebase.vertexai.common.JSON import com.google.firebase.vertexai.type.Candidate @@ -33,8 +34,11 @@ import io.ktor.http.headersOf import io.ktor.utils.io.ByteChannel import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString +import org.mockito.Mockito private val TEST_CLIENT_ID = "genai-android/test" +private val TEST_APP_ID = "1:android:12345" +private val TEST_VERSION = 1 internal fun prepareStreamingResponse( response: List @@ -90,6 +94,9 @@ internal fun commonTest( requestOptions: RequestOptions = RequestOptions(), block: CommonTest, ) = doBlocking { + val mockFirebaseApp = Mockito.mock() + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + val channel = ByteChannel(autoFlush = true) val apiController = APIController( @@ -100,6 +107,9 @@ internal fun commonTest( respond(channel, status, headersOf(HttpHeaders.ContentType, "application/json")) }, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) CommonTestScope(channel, apiController).block() diff --git a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt index 4f648735396..a683c1d5032 100644 --- a/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt +++ b/firebase-vertexai/src/test/java/com/google/firebase/vertexai/util/tests.kt @@ -18,6 +18,7 @@ package com.google.firebase.vertexai.util +import com.google.firebase.FirebaseApp import com.google.firebase.vertexai.GenerativeModel import com.google.firebase.vertexai.ImagenModel import com.google.firebase.vertexai.common.APIController @@ -35,8 +36,11 @@ import io.ktor.utils.io.close import io.ktor.utils.io.writeFully import java.io.File import kotlinx.coroutines.launch +import org.mockito.Mockito private val TEST_CLIENT_ID = "firebase-vertexai-android/test" +private val TEST_APP_ID = "1:android:12345" +private val TEST_VERSION = 1 /** String separator used in SSE communication to signal the end of a message. */ internal const val SSE_SEPARATOR = "\r\n\r\n" @@ -100,6 +104,9 @@ internal fun commonTest( block: CommonTest, ) = doBlocking { val channel = ByteChannel(autoFlush = true) + val mockFirebaseApp = Mockito.mock() + Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + val apiController = APIController( "super_cool_test_key", @@ -109,6 +116,9 @@ internal fun commonTest( respond(channel, status, headersOf(HttpHeaders.ContentType, "application/json")) }, TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, null, ) val model = GenerativeModel("cool-model-name", controller = apiController) From f67d32d6b056c3801f28fcf2bbc570ae964d8d33 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Tue, 25 Mar 2025 16:37:47 -0400 Subject: [PATCH 068/162] [VertexAI] Log warning for unsupported model names (#6805) Added a warning message to the initializers of GenerativeModel and ImagenModel that is logged when the provided model name does not start with the expected prefix ("gemini-" for GenerativeModel and "imagen-" for ImagenModel). The warning message includes a link to the documentation for supported models. Note: No error is thrown in case the naming scheme is changed in the future, though we would want to update the logic/message at that time. Related iOS PR https://github.com/firebase/firebase-ios-sdk/pull/14610 --- firebase-vertexai/CHANGELOG.md | 2 ++ .../firebase/vertexai/FirebaseVertexAI.kt | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index e922c2d9f65..37846fe233e 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [feature] Emits a warning when attempting to use an incompatible model with + `GenerativeModel` or `ImagenModel`. * [changed] Added new exception type for quota exceeded scenarios. * [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. * [changed] **Breaking Change**: `ImagenInlineImage.data` now returns the raw diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt index 1790ec0c300..c70a134831a 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAI.kt @@ -16,6 +16,7 @@ package com.google.firebase.vertexai +import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app @@ -68,6 +69,15 @@ internal constructor( if (location.trim().isEmpty() || location.contains("/")) { throw InvalidLocationException(location) } + if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Gemini model "${modelName}"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. + """ + .trimIndent() + ) + } return GenerativeModel( "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", firebaseApp.options.apiKey, @@ -103,6 +113,15 @@ internal constructor( if (location.trim().isEmpty() || location.contains("/")) { throw InvalidLocationException(location) } + if (!modelName.startsWith(IMAGEN_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Imagen model "${modelName}"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Imagen model names. + """ + .trimIndent() + ) + } return ImagenModel( "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", firebaseApp.options.apiKey, @@ -136,6 +155,12 @@ internal constructor( val multiResourceComponent = app[FirebaseVertexAIMultiResourceComponent::class.java] return multiResourceComponent.get(location) } + + private const val GEMINI_MODEL_NAME_PREFIX = "gemini-" + + private const val IMAGEN_MODEL_NAME_PREFIX = "imagen-" + + private val TAG = FirebaseVertexAI::class.java.simpleName } } From 5ff9d9576741c6a8fbcdf0507d53da8b08a4f146 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Wed, 26 Mar 2025 08:05:25 -0600 Subject: [PATCH 069/162] Updated internal Crashpad version (#6797) --- firebase-crashlytics-ndk/CHANGELOG.md | 3 ++- firebase-crashlytics-ndk/src/main/jni/Application.mk | 2 +- .../src/main/jni/crashpad/crashpad_util/Android.mk | 1 + .../src/main/jni/crashpad/mini_chromium_base/Android.mk | 1 + firebase-crashlytics-ndk/src/third_party/crashpad | 2 +- firebase-crashlytics-ndk/src/third_party/lss | 2 +- firebase-crashlytics-ndk/src/third_party/mini_chromium | 2 +- 7 files changed, 8 insertions(+), 5 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index dc276b94fc5..4fdaabb8f41 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - +# 19.4.3 +* [changed] Updated internal Crashpad version to commit `8df174`. # 19.4.2 * [changed] Updated `firebase-crashlytics` dependency to v19.4.2 diff --git a/firebase-crashlytics-ndk/src/main/jni/Application.mk b/firebase-crashlytics-ndk/src/main/jni/Application.mk index af0b6b867d1..affe25cf47c 100644 --- a/firebase-crashlytics-ndk/src/main/jni/Application.mk +++ b/firebase-crashlytics-ndk/src/main/jni/Application.mk @@ -1,3 +1,3 @@ APP_ABI := arm64-v8a armeabi-v7a x86_64 x86 APP_STL := c++_static -APP_PLATFORM := android-16 +APP_PLATFORM := android-21 diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk index 56d1a65ea5b..629d69fe687 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk @@ -44,6 +44,7 @@ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/crashpad/util/linux/exception_handler_protocol.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/initial_signal_dispositions.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/memory_map.cc \ + $(THIRD_PARTY_PATH)/crashpad/util/linux/pac_helper.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/proc_stat_reader.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/proc_task_reader.cc \ $(THIRD_PARTY_PATH)/crashpad/util/linux/ptrace_broker.cc \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk index 62cab7de0d4..6ff1e57042b 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk @@ -25,6 +25,7 @@ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/mini_chromium/base/posix/safe_strerror.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/process/memory.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/rand_util.cc \ + $(THIRD_PARTY_PATH)/mini_chromium/base/strings/pattern.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/string_number_conversions.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/string_util.cc \ $(THIRD_PARTY_PATH)/mini_chromium/base/strings/stringprintf.cc \ diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index c902f6b1c9e..8df174c64ca 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c +Subproject commit 8df174c64ca2b9dc0f83b089d30760867966b173 diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss index 9719c1e1e67..ed31caa60f2 160000 --- a/firebase-crashlytics-ndk/src/third_party/lss +++ b/firebase-crashlytics-ndk/src/third_party/lss @@ -1 +1 @@ -Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31 +Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0 diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index 4332ddb6963..8b56c771841 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430 +Subproject commit 8b56c7718412ec7d12d05522f7af0cbb787cbb00 From dbeecd4df3803d14e885b011e5998129c87cfd87 Mon Sep 17 00:00:00 2001 From: emilypgoogle <110422458+emilypgoogle@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:54:05 -0500 Subject: [PATCH 070/162] Add basic Vertex Java compilation tests (#6810) This is a starting point for compilation testing, broadly using most symbols Vertex exposes to Java users, lightly validating usability and structure. As a note, there are some builder patterns in Vertex that don't work as expected from Java and were omitted, as fixing that would be a breaking change. --- .../firebase-vertexai.gradle.kts | 1 + .../firebase/vertexai/JavaCompileTests.java | 239 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 firebase-vertexai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java diff --git a/firebase-vertexai/firebase-vertexai.gradle.kts b/firebase-vertexai/firebase-vertexai.gradle.kts index 08942b6bedf..6e2e604d26f 100644 --- a/firebase-vertexai/firebase-vertexai.gradle.kts +++ b/firebase-vertexai/firebase-vertexai.gradle.kts @@ -64,6 +64,7 @@ android { } } lint { targetSdk = targetSdkVersion } + sourceSets { getByName("test").java.srcDirs("src/testUtil") } } // Enable Kotlin "Explicit API Mode". This causes the Kotlin compiler to fail if any diff --git a/firebase-vertexai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java b/firebase-vertexai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java new file mode 100644 index 00000000000..066e672ffb8 --- /dev/null +++ b/firebase-vertexai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java @@ -0,0 +1,239 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package java.com.google.firebase.vertexai; + +import android.graphics.Bitmap; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.firebase.concurrent.FirebaseExecutors; +import com.google.firebase.vertexai.FirebaseVertexAI; +import com.google.firebase.vertexai.GenerativeModel; +import com.google.firebase.vertexai.java.ChatFutures; +import com.google.firebase.vertexai.java.GenerativeModelFutures; +import com.google.firebase.vertexai.type.BlockReason; +import com.google.firebase.vertexai.type.Candidate; +import com.google.firebase.vertexai.type.Citation; +import com.google.firebase.vertexai.type.CitationMetadata; +import com.google.firebase.vertexai.type.Content; +import com.google.firebase.vertexai.type.ContentModality; +import com.google.firebase.vertexai.type.CountTokensResponse; +import com.google.firebase.vertexai.type.FileDataPart; +import com.google.firebase.vertexai.type.FinishReason; +import com.google.firebase.vertexai.type.FunctionCallPart; +import com.google.firebase.vertexai.type.GenerateContentResponse; +import com.google.firebase.vertexai.type.HarmCategory; +import com.google.firebase.vertexai.type.HarmProbability; +import com.google.firebase.vertexai.type.HarmSeverity; +import com.google.firebase.vertexai.type.ImagePart; +import com.google.firebase.vertexai.type.InlineDataPart; +import com.google.firebase.vertexai.type.ModalityTokenCount; +import com.google.firebase.vertexai.type.Part; +import com.google.firebase.vertexai.type.PromptFeedback; +import com.google.firebase.vertexai.type.SafetyRating; +import com.google.firebase.vertexai.type.TextPart; +import com.google.firebase.vertexai.type.UsageMetadata; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import kotlinx.serialization.json.JsonElement; +import kotlinx.serialization.json.JsonNull; +import org.junit.Assert; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * Tests in this file exist to be compiled, not invoked + */ +public class JavaCompileTests { + + public void initializeJava() throws Exception { + FirebaseVertexAI vertex = FirebaseVertexAI.getInstance(); + GenerativeModel model = vertex.generativeModel("fake-model-name"); + GenerativeModelFutures futures = GenerativeModelFutures.from(model); + testFutures(futures); + } + + private void testFutures(GenerativeModelFutures futures) throws Exception { + Content content = + new Content.Builder() + .addText("Fake prompt") + .addFileData("fakeuri", "image/png") + .addInlineData(new byte[] {}, "text/json") + .addImage(Bitmap.createBitmap(0, 0, Bitmap.Config.HARDWARE)) + .addPart(new FunctionCallPart("fakeFunction", Map.of("fakeArg", JsonNull.INSTANCE))) + .build(); + // TODO b/406558430 Content.Builder.setParts and Content.Builder.setRole return void + Executor executor = FirebaseExecutors.directExecutor(); + ListenableFuture countResponse = futures.countTokens(content); + validateCountTokensResponse(countResponse.get()); + ListenableFuture generateResponse = futures.generateContent(content); + validateGenerateContentResponse(generateResponse.get()); + ChatFutures chat = futures.startChat(); + ListenableFuture future = chat.sendMessage(content); + future.addListener( + () -> { + try { + validateGenerateContentResponse(future.get()); + } catch (Exception e) { + // Ignore + } + }, + executor); + Publisher responsePublisher = futures.generateContentStream(content); + responsePublisher.subscribe( + new Subscriber() { + private boolean complete = false; + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(GenerateContentResponse response) { + Assert.assertFalse(complete); + validateGenerateContentResponse(response); + } + + @Override + public void onError(Throwable t) { + // Ignore + } + + @Override + public void onComplete() { + complete = true; + } + }); + } + + public void validateCountTokensResponse(CountTokensResponse response) { + int tokens = response.getTotalTokens(); + Integer billable = response.getTotalBillableCharacters(); + Assert.assertEquals(tokens, response.component1()); + Assert.assertEquals(billable, response.component2()); + Assert.assertEquals(response.getPromptTokensDetails(), response.component3()); + for (ModalityTokenCount count : response.getPromptTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + } + + public void validateGenerateContentResponse(GenerateContentResponse response) { + List candidates = response.getCandidates(); + if (candidates.size() == 1 + && candidates.get(0).getContent().getParts().stream() + .anyMatch(p -> p instanceof TextPart && !((TextPart) p).getText().isEmpty())) { + String text = response.getText(); + Assert.assertNotNull(text); + Assert.assertFalse(text.isBlank()); + } + validateCandidates(candidates); + validateFunctionCalls(response.getFunctionCalls()); + validatePromptFeedback(response.getPromptFeedback()); + validateUsageMetadata(response.getUsageMetadata()); + } + + public void validateCandidates(List candidates) { + for (Candidate candidate : candidates) { + validateCitationMetadata(candidate.getCitationMetadata()); + FinishReason reason = candidate.getFinishReason(); + validateSafetyRatings(candidate.getSafetyRatings()); + validateCitationMetadata(candidate.getCitationMetadata()); + validateContent(candidate.getContent()); + } + } + + public void validateContent(Content content) { + String role = content.getRole(); + for (Part part : content.getParts()) { + if (part instanceof TextPart) { + String text = ((TextPart) part).getText(); + } else if (part instanceof ImagePart) { + Bitmap bitmap = ((ImagePart) part).getImage(); + } else if (part instanceof InlineDataPart) { + String mime = ((InlineDataPart) part).getMimeType(); + byte[] data = ((InlineDataPart) part).getInlineData(); + } else if (part instanceof FileDataPart) { + String mime = ((FileDataPart) part).getMimeType(); + String uri = ((FileDataPart) part).getUri(); + } + } + } + + public void validateCitationMetadata(CitationMetadata metadata) { + if (metadata != null) { + for (Citation citation : metadata.getCitations()) { + String uri = citation.getUri(); + String license = citation.getLicense(); + Calendar calendar = citation.getPublicationDate(); + int startIndex = citation.getStartIndex(); + int endIndex = citation.getEndIndex(); + Assert.assertTrue(startIndex <= endIndex); + } + } + } + + public void validateFunctionCalls(List parts) { + if (parts != null) { + for (FunctionCallPart part : parts) { + String functionName = part.getName(); + Map args = part.getArgs(); + Assert.assertFalse(functionName.isBlank()); + } + } + } + + public void validatePromptFeedback(PromptFeedback feedback) { + if (feedback != null) { + String message = feedback.getBlockReasonMessage(); + BlockReason reason = feedback.getBlockReason(); + validateSafetyRatings(feedback.getSafetyRatings()); + } + } + + public void validateSafetyRatings(List ratings) { + for (SafetyRating rating : ratings) { + Boolean blocked = rating.getBlocked(); + HarmCategory category = rating.getCategory(); + HarmProbability probability = rating.getProbability(); + float score = rating.getProbabilityScore(); + HarmSeverity severity = rating.getSeverity(); + Float severityScore = rating.getSeverityScore(); + if (severity != null) { + Assert.assertNotNull(severityScore); + } + } + } + + public void validateUsageMetadata(UsageMetadata metadata) { + if (metadata != null) { + int totalTokens = metadata.getTotalTokenCount(); + int promptTokenCount = metadata.getPromptTokenCount(); + for (ModalityTokenCount count : metadata.getPromptTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + Integer candidatesTokenCount = metadata.getCandidatesTokenCount(); + for (ModalityTokenCount count : metadata.getCandidatesTokensDetails()) { + ContentModality modality = count.getModality(); + int tokenCount = count.getTokenCount(); + } + } + } +} From cf2b7a86ff988ce9d37d7390520547f54a36381c Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 27 Mar 2025 09:19:01 -0600 Subject: [PATCH 071/162] Upgrade to Android ndk r27c and update Crashpad to latest commit (#6814) --- firebase-crashlytics-ndk/CHANGELOG.md | 2 +- firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle | 2 +- .../src/main/jni/crashpad/crashpad_client/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_compat/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_handler_lib/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_minidump/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_snapshot/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_tool_support/Android.mk | 2 +- .../src/main/jni/crashpad/crashpad_util/Android.mk | 2 +- .../src/main/jni/crashpad/mini_chromium_base/Android.mk | 2 +- .../src/main/jni/libcrashlytics-common/Android.mk | 2 +- .../src/main/jni/libcrashlytics-handler/Android.mk | 2 +- .../src/main/jni/libcrashlytics-trampoline/Android.mk | 2 +- firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk | 2 +- firebase-crashlytics-ndk/src/third_party/crashpad | 2 +- firebase-crashlytics-ndk/src/third_party/mini_chromium | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index 4fdaabb8f41..a83b12613c4 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased # 19.4.3 -* [changed] Updated internal Crashpad version to commit `8df174`. +* [changed] Updated internal Crashpad version to commit `21a20e`. # 19.4.2 * [changed] Updated `firebase-crashlytics` dependency to v19.4.2 diff --git a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle index aafc02f489c..4cf82e95069 100644 --- a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle +++ b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle @@ -37,7 +37,7 @@ android { timeOutInMs 60 * 1000 } namespace "com.google.firebase.crashlytics.ndk" - ndkVersion "25.1.8937393" + ndkVersion "27.2.12479018" compileSdkVersion project.compileSdkVersion defaultConfig { minSdkVersion project.minSdkVersion diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk index f08db85fed0..db8e082111c 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_client/Android.mk @@ -17,7 +17,7 @@ LOCAL_CPPFLAGS := \ -Wall \ -Os \ -flto \ - -std=c++17 \ + -std=c++20 \ LOCAL_SRC_FILES := \ $(THIRD_PARTY_PATH)/crashpad/client/annotation.cc \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk index ddec4ff67a6..e3157757580 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_compat/Android.mk @@ -21,7 +21,7 @@ LOCAL_EXPORT_C_INCLUDES := \ LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ -Wall \ - -std=c++17 \ + -std=c++20 \ -Os \ -flto \ -fvisibility=hidden \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk index 94928ae917d..4b5f4e31b09 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_handler_lib/Android.mk @@ -13,7 +13,7 @@ LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ -DCRASHPAD_ZLIB_SOURCE_SYSTEM \ -Wall \ - -std=c++17 \ + -std=c++20 \ -Os \ -flto \ -fvisibility=hidden \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk index 3a717888749..8c5309bf003 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_minidump/Android.mk @@ -8,7 +8,7 @@ LOCAL_MODULE := crashpad_minidump LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk index de07187cf21..60f2eda169b 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_snapshot/Android.mk @@ -8,7 +8,7 @@ LOCAL_MODULE := crashpad_snapshot LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk index 11c2e0c51e2..b7b1281bae6 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_tool_support/Android.mk @@ -9,7 +9,7 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/crashpad LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk index 629d69fe687..0b75dc36e53 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/crashpad_util/Android.mk @@ -18,7 +18,7 @@ LOCAL_CPPFLAGS := \ -DZLIB_CONST \ -DCRASHPAD_ZLIB_SOURCE_SYSTEM \ -DCRASHPAD_LSS_SOURCE_EXTERNAL \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk index 6ff1e57042b..a2a5b656c47 100644 --- a/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/crashpad/mini_chromium_base/Android.mk @@ -10,7 +10,7 @@ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/mini_chromium LOCAL_CPPFLAGS := \ -D_FILE_OFFSET_BITS=64 \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -flto \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk index fcd45bd0b4f..bf322f3a873 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-common/Android.mk @@ -18,7 +18,7 @@ LOCAL_C_INCLUDES := \ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk index ac89324387b..1dc59d2d7cc 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-handler/Android.mk @@ -12,7 +12,7 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/../libcrashlytics-common/include \ LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk index 2eb43b255fb..46114835893 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics-trampoline/Android.mk @@ -9,7 +9,7 @@ endif LOCAL_MODULE := crashlytics-trampoline LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk index 12c9f2088ce..d62658e9112 100644 --- a/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk +++ b/firebase-crashlytics-ndk/src/main/jni/libcrashlytics/Android.mk @@ -16,7 +16,7 @@ LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/$(THIRD_PARTY_PATH)/mini_chromium \ LOCAL_CPPFLAGS := \ - -std=c++17 \ + -std=c++20 \ -Wall \ -Os \ -s \ diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index 8df174c64ca..21a20ef8adf 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit 8df174c64ca2b9dc0f83b089d30760867966b173 +Subproject commit 21a20ef8adf3949de8dd65758a16f83aab344b3c diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index 8b56c771841..7477036e238 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit 8b56c7718412ec7d12d05522f7af0cbb787cbb00 +Subproject commit 7477036e238e54f220bed206f71036db8064dd34 From 9da772ef04db3479749c2d2a6bcf325109c59d52 Mon Sep 17 00:00:00 2001 From: Daymon <17409137+daymxn@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:58:24 -0500 Subject: [PATCH 072/162] Fix startup check failures (#6820) Per [b/407077784](https://b.corp.google.com/issues/407077784), This bumps the `compileSdkVersion` and `targetSdkVersion` within our health metrics test app and benchmarks to match what our SDKs actually use. This also fixes the issue with our startup checks failing due to certain SDK dependencies requiring a higher compile target. --- health-metrics/benchmark/template/app/build.gradle.mustache | 4 ++-- health-metrics/benchmark/template/macrobenchmark/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/health-metrics/benchmark/template/app/build.gradle.mustache b/health-metrics/benchmark/template/app/build.gradle.mustache index 77279ae25d9..a8bf11948eb 100644 --- a/health-metrics/benchmark/template/app/build.gradle.mustache +++ b/health-metrics/benchmark/template/app/build.gradle.mustache @@ -21,14 +21,14 @@ plugins { } android { - compileSdkVersion 32 + compileSdkVersion 34 namespace "com.google.firebase.benchmark" defaultConfig { applicationId 'com.google.firebase.benchmark' minSdkVersion 29 - targetSdkVersion 32 + targetSdkVersion 34 versionCode 1 versionName '1.0' diff --git a/health-metrics/benchmark/template/macrobenchmark/build.gradle b/health-metrics/benchmark/template/macrobenchmark/build.gradle index 8b4556eee76..086de25e4a2 100644 --- a/health-metrics/benchmark/template/macrobenchmark/build.gradle +++ b/health-metrics/benchmark/template/macrobenchmark/build.gradle @@ -33,7 +33,7 @@ android { defaultConfig { minSdk 29 - targetSdk 32 + targetSdk 34 testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } From e65e93f70c2f5b51184545c794bd577f938b904b Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Fri, 28 Mar 2025 18:37:13 -0400 Subject: [PATCH 073/162] [Functions] Bump all test timeouts to 10 seconds (#6821) Integration tests are being flaky due to short timeouts. no-changelog --- .../java/com/google/firebase/functions/StreamTests.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt index 8ef45478fff..8e0d26bff3e 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/StreamTests.kt @@ -102,7 +102,7 @@ class StreamTests { val flow = function.stream(input).asFlow() try { - withTimeout(1000) { + withTimeout(10_000) { flow.collect { response -> if (response is StreamResponse.Message) { messages.add(response) @@ -146,14 +146,14 @@ class StreamTests { @Test fun nonExistentFunction_receivesError() = runBlocking { val function = - functions.getHttpsCallable("nonexistentFunction").withTimeout(2000, TimeUnit.MILLISECONDS) + functions.getHttpsCallable("nonexistentFunction").withTimeout(10_000, TimeUnit.MILLISECONDS) val subscriber = StreamSubscriber() function.stream().subscribe(subscriber) - withTimeout(2000) { + withTimeout(10_000) { while (subscriber.throwable == null) { - delay(100) + delay(1_000) } } @@ -195,7 +195,7 @@ class StreamTests { function.stream(mapOf("data" to "test")).subscribe(subscriber) - withTimeout(2000) { delay(500) } + withTimeout(10_000) { delay(1000) } assertThat(subscriber.throwable).isNull() assertThat(subscriber.messages).isEmpty() assertThat(subscriber.result).isNull() From d2e72df5231cfe0a8067e139ce60aa108992972d Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Fri, 28 Mar 2025 19:22:41 -0400 Subject: [PATCH 074/162] Bidirectional Streaming Android (#6759) Bidirectional streaming for android. Creates a bunch of helper classes for the same. The main classes which handle the bidirectional streaming are LiveGenerativeModel and LiveSession --------- Co-authored-by: VinayGuthal Co-authored-by: Rodrigo Lazo Paz Co-authored-by: Rodrigo Lazo Co-authored-by: Daymon <17409137+daymxn@users.noreply.github.com> --- firebase-vertexai/CHANGELOG.md | 1 + firebase-vertexai/api.txt | 162 ++++++++ .../firebase-vertexai.gradle.kts | 6 +- firebase-vertexai/lint-baseline.xml | 30 ++ .../src/main/AndroidManifest.xml | 6 +- .../firebase/vertexai/FirebaseVertexAI.kt | 53 ++- .../FirebaseVertexAIMultiResourceComponent.kt | 16 +- .../vertexai/FirebaseVertexAIRegistrar.kt | 7 + .../firebase/vertexai/LiveGenerativeModel.kt | 123 ++++++ .../firebase/vertexai/common/APIController.kt | 9 + .../vertexai/java/LiveModelFutures.kt | 53 +++ .../vertexai/java/LiveSessionFutures.kt | 138 +++++++ .../firebase/vertexai/type/AudioHelper.kt | 120 ++++++ .../type/BidiGenerateContentClientMessage.kt | 44 +++ .../firebase/vertexai/type/ContentModality.kt | 9 + .../firebase/vertexai/type/Exceptions.kt | 14 + .../vertexai/type/LiveContentResponse.kt | 42 ++ .../vertexai/type/LiveGenerationConfig.kt | 217 +++++++++++ .../firebase/vertexai/type/LiveSession.kt | 358 ++++++++++++++++++ .../firebase/vertexai/type/MediaData.kt | 40 ++ .../com/google/firebase/vertexai/type/Part.kt | 8 +- .../vertexai/type/ResponseModality.kt | 66 ++++ .../firebase/vertexai/type/SpeechConfig.kt | 37 ++ .../google/firebase/vertexai/type/Voices.kt | 69 ++++ gradle/libs.versions.toml | 1 + 25 files changed, 1620 insertions(+), 9 deletions(-) create mode 100644 firebase-vertexai/lint-baseline.xml create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/LiveGenerativeModel.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveModelFutures.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveSessionFutures.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/AudioHelper.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/BidiGenerateContentClientMessage.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveContentResponse.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveGenerationConfig.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveSession.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/MediaData.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ResponseModality.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/SpeechConfig.kt create mode 100644 firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Voices.kt diff --git a/firebase-vertexai/CHANGELOG.md b/firebase-vertexai/CHANGELOG.md index 37846fe233e..5619ab98447 100644 --- a/firebase-vertexai/CHANGELOG.md +++ b/firebase-vertexai/CHANGELOG.md @@ -3,6 +3,7 @@ `GenerativeModel` or `ImagenModel`. * [changed] Added new exception type for quota exceeded scenarios. * [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. +* [feature] Added preliminary support for bidirectional streaming. This feature is not yet fully supported. * [changed] **Breaking Change**: `ImagenInlineImage.data` now returns the raw image bytes (in JPEG or PNG format, as specified in `ImagenInlineImage.mimeType`) instead of Base64-encoded data. (#6800) diff --git a/firebase-vertexai/api.txt b/firebase-vertexai/api.txt index 76491378d88..eb7eabdddf2 100644 --- a/firebase-vertexai/api.txt +++ b/firebase-vertexai/api.txt @@ -29,6 +29,11 @@ package com.google.firebase.vertexai { method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null); method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.vertexai.type.ImagenSafetySettings? safetySettings = null); method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.ImagenModel imagenModel(String modelName, com.google.firebase.vertexai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.vertexai.type.ImagenSafetySettings? safetySettings = null, com.google.firebase.vertexai.type.RequestOptions requestOptions = com.google.firebase.vertexai.type.RequestOptions()); + method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.LiveGenerativeModel liveModel(String modelName); + method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.vertexai.type.LiveGenerationConfig? generationConfig = null); + method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.vertexai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null); + method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.vertexai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.vertexai.type.Content? systemInstruction = null); + method @com.google.firebase.vertexai.type.PublicPreviewAPI public com.google.firebase.vertexai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.vertexai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.vertexai.type.Content? systemInstruction = null, com.google.firebase.vertexai.type.RequestOptions requestOptions = com.google.firebase.vertexai.type.RequestOptions()); property public static final com.google.firebase.vertexai.FirebaseVertexAI instance; field public static final com.google.firebase.vertexai.FirebaseVertexAI.Companion Companion; } @@ -63,6 +68,10 @@ package com.google.firebase.vertexai { method public suspend Object? generateImages(String prompt, kotlin.coroutines.Continuation>); } + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class LiveGenerativeModel { + method public suspend Object? connect(kotlin.coroutines.Continuation); + } + } package com.google.firebase.vertexai.java { @@ -105,10 +114,42 @@ package com.google.firebase.vertexai.java { method public com.google.firebase.vertexai.java.ImagenModelFutures from(com.google.firebase.vertexai.ImagenModel model); } + @com.google.firebase.vertexai.type.PublicPreviewAPI public abstract class LiveModelFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture connect(); + method public static final com.google.firebase.vertexai.java.LiveModelFutures from(com.google.firebase.vertexai.LiveGenerativeModel model); + field public static final com.google.firebase.vertexai.java.LiveModelFutures.Companion Companion; + } + + public static final class LiveModelFutures.Companion { + method public com.google.firebase.vertexai.java.LiveModelFutures from(com.google.firebase.vertexai.LiveGenerativeModel model); + } + + @com.google.firebase.vertexai.type.PublicPreviewAPI public abstract class LiveSessionFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture close(); + method public static final com.google.firebase.vertexai.java.LiveSessionFutures from(com.google.firebase.vertexai.type.LiveSession session); + method public abstract org.reactivestreams.Publisher receive(); + method public abstract com.google.common.util.concurrent.ListenableFuture send(com.google.firebase.vertexai.type.Content content); + method public abstract com.google.common.util.concurrent.ListenableFuture send(String text); + method public abstract com.google.common.util.concurrent.ListenableFuture sendFunctionResponse(java.util.List functionList); + method public abstract com.google.common.util.concurrent.ListenableFuture sendMediaStream(java.util.List mediaChunks); + method public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler); + method public abstract com.google.common.util.concurrent.ListenableFuture stopAudioConversation(); + method public abstract void stopReceiving(); + field public static final com.google.firebase.vertexai.java.LiveSessionFutures.Companion Companion; + } + + public static final class LiveSessionFutures.Companion { + method public com.google.firebase.vertexai.java.LiveSessionFutures from(com.google.firebase.vertexai.type.LiveSession session); + } + } package com.google.firebase.vertexai.type { + public final class AudioRecordInitializationFailedException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { + ctor public AudioRecordInitializationFailedException(String message); + } + public final class BlockReason { method public String getName(); method public int getOrdinal(); @@ -520,6 +561,85 @@ package com.google.firebase.vertexai.type { public final class InvalidStateException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { } + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class LiveContentResponse { + method public com.google.firebase.vertexai.type.Content? getData(); + method public java.util.List? getFunctionCalls(); + method public int getStatus(); + method public String? getText(); + property public final com.google.firebase.vertexai.type.Content? data; + property public final java.util.List? functionCalls; + property public final int status; + property public final String? text; + } + + @kotlin.jvm.JvmInline public static final value class LiveContentResponse.Status { + field public static final com.google.firebase.vertexai.type.LiveContentResponse.Status.Companion Companion; + } + + public static final class LiveContentResponse.Status.Companion { + method public int getINTERRUPTED(); + method public int getNORMAL(); + method public int getTURN_COMPLETE(); + property public final int INTERRUPTED; + property public final int NORMAL; + property public final int TURN_COMPLETE; + } + + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class LiveGenerationConfig { + field public static final com.google.firebase.vertexai.type.LiveGenerationConfig.Companion Companion; + } + + public static final class LiveGenerationConfig.Builder { + ctor public LiveGenerationConfig.Builder(); + method public com.google.firebase.vertexai.type.LiveGenerationConfig build(); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setCandidateCount(Integer? candidateCount); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setFrequencyPenalty(Float? frequencyPenalty); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setMaxOutputTokens(Integer? maxOutputTokens); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setPresencePenalty(Float? presencePenalty); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setResponseModalities(com.google.firebase.vertexai.type.ResponseModality? responseModalities); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setSpeechConfig(com.google.firebase.vertexai.type.SpeechConfig? speechConfig); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setTemperature(Float? temperature); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setTopK(Integer? topK); + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder setTopP(Float? topP); + field public Integer? candidateCount; + field public Float? frequencyPenalty; + field public Integer? maxOutputTokens; + field public Float? presencePenalty; + field public com.google.firebase.vertexai.type.ResponseModality? responseModality; + field public com.google.firebase.vertexai.type.SpeechConfig? speechConfig; + field public Float? temperature; + field public Integer? topK; + field public Float? topP; + } + + public static final class LiveGenerationConfig.Companion { + method public com.google.firebase.vertexai.type.LiveGenerationConfig.Builder builder(); + } + + public final class LiveGenerationConfigKt { + method public static com.google.firebase.vertexai.type.LiveGenerationConfig liveGenerationConfig(kotlin.jvm.functions.Function1 init); + } + + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class LiveSession { + method public suspend Object? close(kotlin.coroutines.Continuation); + method public kotlinx.coroutines.flow.Flow receive(); + method public suspend Object? send(com.google.firebase.vertexai.type.Content content, kotlin.coroutines.Continuation); + method public suspend Object? send(String text, kotlin.coroutines.Continuation); + method public suspend Object? sendFunctionResponse(java.util.List functionList, kotlin.coroutines.Continuation); + method public suspend Object? sendMediaStream(java.util.List mediaChunks, kotlin.coroutines.Continuation); + method public suspend Object? startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler = null, kotlin.coroutines.Continuation); + method public void stopAudioConversation(); + method public void stopReceiving(); + } + + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class MediaData { + ctor public MediaData(byte[] data, String mimeType); + method public byte[] getData(); + method public String getMimeType(); + property public final byte[] data; + property public final String mimeType; + } + public final class ModalityTokenCount { method public operator com.google.firebase.vertexai.type.ContentModality component1(); method public operator int component2(); @@ -568,6 +688,19 @@ package com.google.firebase.vertexai.type { public final class RequestTimeoutException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { } + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class ResponseModality { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.vertexai.type.ResponseModality AUDIO; + field public static final com.google.firebase.vertexai.type.ResponseModality.Companion Companion; + field public static final com.google.firebase.vertexai.type.ResponseModality IMAGE; + field public static final com.google.firebase.vertexai.type.ResponseModality TEXT; + field public static final com.google.firebase.vertexai.type.ResponseModality UNSPECIFIED; + } + + public static final class ResponseModality.Companion { + } + public final class ResponseStoppedException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { method public com.google.firebase.vertexai.type.GenerateContentResponse getResponse(); property public final com.google.firebase.vertexai.type.GenerateContentResponse response; @@ -679,9 +812,23 @@ package com.google.firebase.vertexai.type { public final class ServerException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { } + public final class ServiceConnectionHandshakeFailedException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { + ctor public ServiceConnectionHandshakeFailedException(String message, Throwable? cause = null); + } + public final class ServiceDisabledException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { } + public final class SessionAlreadyReceivingException extends com.google.firebase.vertexai.type.FirebaseVertexAIException { + ctor public SessionAlreadyReceivingException(); + } + + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class SpeechConfig { + ctor public SpeechConfig(com.google.firebase.vertexai.type.Voices voice); + method public com.google.firebase.vertexai.type.Voices getVoice(); + property public final com.google.firebase.vertexai.type.Voices voice; + } + public abstract class StringFormat { } @@ -728,5 +875,20 @@ package com.google.firebase.vertexai.type { property public final int totalTokenCount; } + @com.google.firebase.vertexai.type.PublicPreviewAPI public final class Voices { + method public int getOrdinal(); + property public final int ordinal; + field public static final com.google.firebase.vertexai.type.Voices AOEDE; + field public static final com.google.firebase.vertexai.type.Voices CHARON; + field public static final com.google.firebase.vertexai.type.Voices.Companion Companion; + field public static final com.google.firebase.vertexai.type.Voices FENRIR; + field public static final com.google.firebase.vertexai.type.Voices KORE; + field public static final com.google.firebase.vertexai.type.Voices PUCK; + field public static final com.google.firebase.vertexai.type.Voices UNSPECIFIED; + } + + public static final class Voices.Companion { + } + } diff --git a/firebase-vertexai/firebase-vertexai.gradle.kts b/firebase-vertexai/firebase-vertexai.gradle.kts index 6e2e604d26f..f728e905cbb 100644 --- a/firebase-vertexai/firebase-vertexai.gradle.kts +++ b/firebase-vertexai/firebase-vertexai.gradle.kts @@ -63,7 +63,10 @@ android { isReturnDefaultValues = true } } - lint { targetSdk = targetSdkVersion } + lint { + targetSdk = targetSdkVersion + baseline = file("lint-baseline.xml") + } sourceSets { getByName("test").java.srcDirs("src/testUtil") } } @@ -84,6 +87,7 @@ tasks.withType().all { dependencies { implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.core) + implementation(libs.ktor.client.websockets) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.logging) diff --git a/firebase-vertexai/lint-baseline.xml b/firebase-vertexai/lint-baseline.xml new file mode 100644 index 00000000000..5f6b1f3ebfd --- /dev/null +++ b/firebase-vertexai/lint-baseline.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/firebase-vertexai/src/main/AndroidManifest.xml b/firebase-vertexai/src/main/AndroidManifest.xml index f61156bd1b5..1a791682f4b 100644 --- a/firebase-vertexai/src/main/AndroidManifest.xml +++ b/firebase-vertexai/src/main/AndroidManifest.xml @@ -20,7 +20,11 @@ - + + + + + , private val internalAuthProvider: Provider, @@ -46,7 +50,7 @@ internal constructor( /** * Instantiates a new [GenerativeModel] given the provided parameters. * - * @param modelName The name of the model to use, for example `"gemini-1.5-pro"`. + * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. * @param generationConfig The configuration parameters to use for content generation. * @param safetySettings The safety bounds the model will abide to during content generation. * @param tools A list of [Tool]s the model may use to generate content. @@ -93,6 +97,53 @@ internal constructor( ) } + /** + * Instantiates a new [LiveGenerationConfig] given the provided parameters. + * + * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. + * @param generationConfig The configuration parameters to use for content generation. + * @param tools A list of [Tool]s the model may use to generate content. + * @param systemInstruction [Content] instructions that direct the model to behave a certain way. + * Currently only text content is supported. + * @param requestOptions Configuration options for sending requests to the backend. + * @return The initialized [LiveGenerativeModel] instance. + */ + @JvmOverloads + @PublicPreviewAPI + public fun liveModel( + modelName: String, + generationConfig: LiveGenerationConfig? = null, + tools: List? = null, + systemInstruction: Content? = null, + requestOptions: RequestOptions = RequestOptions(), + ): LiveGenerativeModel { + if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { + Log.w( + TAG, + """Unsupported Gemini model "$modelName"; see + https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. + """ + .trimIndent() + ) + } + if (location.trim().isEmpty() || location.contains("/")) { + throw InvalidLocationException(location) + } + return LiveGenerativeModel( + "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", + firebaseApp.options.apiKey, + firebaseApp, + backgroundDispatcher, + generationConfig, + tools, + systemInstruction, + location, + requestOptions, + appCheckProvider.get(), + internalAuthProvider.get(), + ) + } + /** * Instantiates a new [ImagenModel] given the provided parameters. * diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIMultiResourceComponent.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIMultiResourceComponent.kt index 213351fdc92..1b9cb7a4909 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIMultiResourceComponent.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIMultiResourceComponent.kt @@ -18,9 +18,11 @@ package com.google.firebase.vertexai import androidx.annotation.GuardedBy import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.inject.Provider +import kotlin.coroutines.CoroutineContext /** * Multi-resource container for Firebase Vertex AI. @@ -29,8 +31,9 @@ import com.google.firebase.inject.Provider */ internal class FirebaseVertexAIMultiResourceComponent( private val app: FirebaseApp, + @Background val backgroundDispatcher: CoroutineContext, private val appCheckProvider: Provider, - private val internalAuthProvider: Provider + private val internalAuthProvider: Provider, ) { @GuardedBy("this") private val instances: MutableMap = mutableMapOf() @@ -38,8 +41,13 @@ internal class FirebaseVertexAIMultiResourceComponent( fun get(location: String): FirebaseVertexAI = synchronized(this) { instances[location] - ?: FirebaseVertexAI(app, location, appCheckProvider, internalAuthProvider).also { - instances[location] = it - } + ?: FirebaseVertexAI( + app, + backgroundDispatcher, + location, + appCheckProvider, + internalAuthProvider + ) + .also { instances[location] = it } } } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIRegistrar.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIRegistrar.kt index fca48ae395a..ff5409567a9 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIRegistrar.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/FirebaseVertexAIRegistrar.kt @@ -18,13 +18,16 @@ package com.google.firebase.vertexai import androidx.annotation.Keep import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.platforminfo.LibraryVersionComponent +import kotlinx.coroutines.CoroutineDispatcher /** * [ComponentRegistrar] for setting up [FirebaseVertexAI] and its internal dependencies. @@ -38,11 +41,13 @@ internal class FirebaseVertexAIRegistrar : ComponentRegistrar { Component.builder(FirebaseVertexAIMultiResourceComponent::class.java) .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) + .add(Dependency.required(backgroundDispatcher)) .add(Dependency.optionalProvider(appCheckInterop)) .add(Dependency.optionalProvider(internalAuthProvider)) .factory { container -> FirebaseVertexAIMultiResourceComponent( container[firebaseApp], + container.get(backgroundDispatcher), container.getProvider(appCheckInterop), container.getProvider(internalAuthProvider) ) @@ -57,5 +62,7 @@ internal class FirebaseVertexAIRegistrar : ComponentRegistrar { private val firebaseApp = unqualified(FirebaseApp::class.java) private val appCheckInterop = unqualified(InteropAppCheckTokenProvider::class.java) private val internalAuthProvider = unqualified(InternalAuthProvider::class.java) + private val backgroundDispatcher = + Qualified.qualified(Background::class.java, CoroutineDispatcher::class.java) } } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/LiveGenerativeModel.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/LiveGenerativeModel.kt new file mode 100644 index 00000000000..e557b694620 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/LiveGenerativeModel.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai + +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.vertexai.common.APIController +import com.google.firebase.vertexai.common.AppCheckHeaderProvider +import com.google.firebase.vertexai.type.BidiGenerateContentClientMessage +import com.google.firebase.vertexai.type.Content +import com.google.firebase.vertexai.type.LiveGenerationConfig +import com.google.firebase.vertexai.type.LiveSession +import com.google.firebase.vertexai.type.PublicPreviewAPI +import com.google.firebase.vertexai.type.RequestOptions +import com.google.firebase.vertexai.type.ServiceConnectionHandshakeFailedException +import com.google.firebase.vertexai.type.Tool +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readBytes +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** + * Represents a multimodal model (like Gemini) capable of real-time content generation based on + * various input types, supporting bidirectional streaming. + */ +@PublicPreviewAPI +public class LiveGenerativeModel +internal constructor( + private val modelName: String, + @Background private val backgroundDispatcher: CoroutineContext, + private val config: LiveGenerationConfig? = null, + private val tools: List? = null, + private val systemInstruction: Content? = null, + private val location: String, + private val controller: APIController +) { + internal constructor( + modelName: String, + apiKey: String, + firebaseApp: FirebaseApp, + backgroundDispatcher: CoroutineContext, + config: LiveGenerationConfig? = null, + tools: List? = null, + systemInstruction: Content? = null, + location: String = "us-central1", + requestOptions: RequestOptions = RequestOptions(), + appCheckTokenProvider: InteropAppCheckTokenProvider? = null, + internalAuthProvider: InternalAuthProvider? = null, + ) : this( + modelName, + backgroundDispatcher, + config, + tools, + systemInstruction, + location, + APIController( + apiKey, + modelName, + requestOptions, + "gl-kotlin/${KotlinVersion.CURRENT} fire/${BuildConfig.VERSION_NAME}", + firebaseApp, + AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + ), + ) + + /** + * Start a [LiveSession] with the server for bidirectional streaming. + * + * @return A [LiveSession] that you can use to stream messages to and from the server. + * @throws [ServiceConnectionHandshakeFailedException] If the client was not able to establish a + * connection with the server. + */ + @OptIn(ExperimentalSerializationApi::class) + public suspend fun connect(): LiveSession { + val clientMessage = + BidiGenerateContentClientMessage( + modelName, + config?.toInternal(), + tools?.map { it.toInternal() }, + systemInstruction?.toInternal() + ) + .toInternal() + val data: String = Json.encodeToString(clientMessage) + try { + val webSession = controller.getWebSocketSession(location) + webSession.send(Frame.Text(data)) + val receivedJson = webSession.incoming.receive().readBytes().toString(Charsets.UTF_8) + // TODO: Try to decode the json instead of string matching. + return if (receivedJson.contains("setupComplete")) { + LiveSession(session = webSession, backgroundDispatcher = backgroundDispatcher) + } else { + webSession.close() + throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") + } + } catch (e: ClosedReceiveChannelException) { + throw ServiceConnectionHandshakeFailedException("Channel was closed by the server", e) + } + } + + private companion object { + private val TAG = LiveGenerativeModel::class.java.simpleName + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt index c67e21ccf23..da580429f8c 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/APIController.kt @@ -36,6 +36,9 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocketSession import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.header import io.ktor.client.request.post @@ -126,6 +129,7 @@ internal constructor( socketTimeoutMillis = max(180.seconds.inWholeMilliseconds, requestOptions.timeout.inWholeMilliseconds) } + install(WebSockets) install(ContentNegotiation) { json(JSON) } } @@ -156,6 +160,11 @@ internal constructor( throw FirebaseCommonAIException.from(e) } + private fun getBidiEndpoint(location: String): String = + "wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/$location?key=$key" + + suspend fun getWebSocketSession(location: String): ClientWebSocketSession = + client.webSocketSession(getBidiEndpoint(location)) fun generateContentStream( request: GenerateContentRequest ): Flow = diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveModelFutures.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveModelFutures.kt new file mode 100644 index 00000000000..c167e700a5e --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveModelFutures.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.vertexai.LiveGenerativeModel +import com.google.firebase.vertexai.type.LiveSession +import com.google.firebase.vertexai.type.PublicPreviewAPI +import com.google.firebase.vertexai.type.ServiceConnectionHandshakeFailedException + +/** + * Wrapper class providing Java compatible methods for [LiveGenerativeModel]. + * + * @see [LiveGenerativeModel] + */ +@PublicPreviewAPI +public abstract class LiveModelFutures internal constructor() { + + /** + * Start a [LiveSession] with the server for bidirectional streaming. + * @return A [LiveSession] that you can use to stream messages to and from the server. + * @throws [ServiceConnectionHandshakeFailedException] If the client was not able to establish a + * connection with the server. + */ + public abstract fun connect(): ListenableFuture + + private class FuturesImpl(private val model: LiveGenerativeModel) : LiveModelFutures() { + override fun connect(): ListenableFuture { + return SuspendToFutureAdapter.launchFuture { model.connect() } + } + } + + public companion object { + + /** @return a [LiveModelFutures] created around the provided [LiveGenerativeModel] */ + @JvmStatic public fun from(model: LiveGenerativeModel): LiveModelFutures = FuturesImpl(model) + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveSessionFutures.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveSessionFutures.kt new file mode 100644 index 00000000000..044f83e8cc1 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/java/LiveSessionFutures.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.java + +import androidx.concurrent.futures.SuspendToFutureAdapter +import com.google.common.util.concurrent.ListenableFuture +import com.google.firebase.vertexai.type.Content +import com.google.firebase.vertexai.type.FunctionCallPart +import com.google.firebase.vertexai.type.FunctionResponsePart +import com.google.firebase.vertexai.type.LiveContentResponse +import com.google.firebase.vertexai.type.LiveSession +import com.google.firebase.vertexai.type.MediaData +import com.google.firebase.vertexai.type.PublicPreviewAPI +import com.google.firebase.vertexai.type.SessionAlreadyReceivingException +import kotlinx.coroutines.reactive.asPublisher +import org.reactivestreams.Publisher + +/** + * Wrapper class providing Java compatible methods for [LiveSession]. + * + * @see [LiveSession] + */ +@PublicPreviewAPI +public abstract class LiveSessionFutures internal constructor() { + + /** + * Starts an audio conversation with the Gemini server, which can only be stopped using + * [stopAudioConversation]. + * + * @param functionCallHandler A callback function to map function calls from the server to their + * response parts. + */ + public abstract fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + ): ListenableFuture + + /** + * Stops the audio conversation with the Gemini Server. + * + * @see [startAudioConversation] + * @see [stopReceiving] + */ + public abstract fun stopAudioConversation(): ListenableFuture + + /** Stop receiving from the server. */ + public abstract fun stopReceiving() + + /** + * Sends the function response from the client to the server. + * + * @param functionList The list of [FunctionResponsePart] instances indicating the function + * response from the client. + */ + public abstract fun sendFunctionResponse( + functionList: List + ): ListenableFuture + + /** + * Streams client data to the server. + * + * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. + */ + public abstract fun sendMediaStream(mediaChunks: List): ListenableFuture + + /** + * Sends [data][Content] to the server. + * + * @param content Client [Content] to be sent to the server. + */ + public abstract fun send(content: Content): ListenableFuture + + /** + * Sends text to the server + * + * @param text Text to be sent to the server. + */ + public abstract fun send(text: String): ListenableFuture + + /** Closes the client session. */ + public abstract fun close(): ListenableFuture + + /** + * Receives responses from the server for both streaming and standard requests. + * + * @return A [Publisher] which will emit [LiveContentResponse] as and when it receives it. + * + * @throws [SessionAlreadyReceivingException] When the session is already receiving. + */ + public abstract fun receive(): Publisher + + private class FuturesImpl(private val session: LiveSession) : LiveSessionFutures() { + + override fun receive(): Publisher = session.receive().asPublisher() + + override fun close(): ListenableFuture = + SuspendToFutureAdapter.launchFuture { session.close() } + + override fun send(text: String) = SuspendToFutureAdapter.launchFuture { session.send(text) } + + override fun send(content: Content) = + SuspendToFutureAdapter.launchFuture { session.send(content) } + + override fun sendFunctionResponse(functionList: List) = + SuspendToFutureAdapter.launchFuture { session.sendFunctionResponse(functionList) } + + override fun sendMediaStream(mediaChunks: List) = + SuspendToFutureAdapter.launchFuture { session.sendMediaStream(mediaChunks) } + + override fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + ) = SuspendToFutureAdapter.launchFuture { session.startAudioConversation(functionCallHandler) } + + override fun stopAudioConversation() = + SuspendToFutureAdapter.launchFuture { session.stopAudioConversation() } + + override fun stopReceiving() = session.stopReceiving() + } + + public companion object { + + /** @return a [LiveSessionFutures] created around the provided [LiveSession] */ + @JvmStatic public fun from(session: LiveSession): LiveSessionFutures = FuturesImpl(session) + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/AudioHelper.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/AudioHelper.kt new file mode 100644 index 00000000000..35edac88db0 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/AudioHelper.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import android.Manifest +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioRecord +import android.media.AudioTrack +import android.media.MediaRecorder +import android.media.audiofx.AcousticEchoCanceler +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +@PublicPreviewAPI +internal class AudioHelper { + + private lateinit var audioRecord: AudioRecord + private lateinit var audioTrack: AudioTrack + private var stopRecording: Boolean = false + + internal fun release() { + stopRecording = true + if (::audioRecord.isInitialized) { + audioRecord.stop() + audioRecord.release() + } + if (::audioTrack.isInitialized) { + audioTrack.stop() + audioTrack.release() + } + } + + internal fun setupAudioTrack() { + audioTrack = + AudioTrack( + AudioManager.STREAM_MUSIC, + 24000, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + AudioTrack.getMinBufferSize( + 24000, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ), + AudioTrack.MODE_STREAM + ) + audioTrack.play() + } + + internal fun playAudio(data: ByteArray) { + if (!stopRecording) { + audioTrack.write(data, 0, data.size) + } + } + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + fun startRecording(): Flow { + + val bufferSize = + AudioRecord.getMinBufferSize( + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + if ( + bufferSize == AudioRecord.ERROR || + bufferSize == AudioRecord.ERROR_BAD_VALUE || + bufferSize <= 0 + ) { + throw AudioRecordInitializationFailedException( + "Audio Record buffer size is invalid (${bufferSize})" + ) + } + audioRecord = + AudioRecord( + MediaRecorder.AudioSource.VOICE_COMMUNICATION, + 16000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize + ) + if (audioRecord.state != AudioRecord.STATE_INITIALIZED) { + throw AudioRecordInitializationFailedException( + "Audio Record initialization has failed. State: ${audioRecord.state}" + ) + } + if (AcousticEchoCanceler.isAvailable()) { + val echoCanceler = AcousticEchoCanceler.create(audioRecord.audioSessionId) + echoCanceler?.enabled = true + } + + audioRecord.startRecording() + + return flow { + val buffer = ByteArray(bufferSize) + while (!stopRecording) { + val bytesRead = audioRecord.read(buffer, 0, buffer.size) + if (bytesRead > 0) { + emit(buffer.copyOf(bytesRead)) + } + } + } + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/BidiGenerateContentClientMessage.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/BidiGenerateContentClientMessage.kt new file mode 100644 index 00000000000..5488cb240f5 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/BidiGenerateContentClientMessage.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable + +@OptIn(ExperimentalSerializationApi::class) +@PublicPreviewAPI +internal class BidiGenerateContentClientMessage( + val model: String, + val generationConfig: LiveGenerationConfig.Internal?, + val tools: List?, + val systemInstruction: Content.Internal? +) { + + @Serializable + internal class Internal(val setup: BidiGenerateContentSetup) { + @Serializable + internal data class BidiGenerateContentSetup( + val model: String, + val generationConfig: LiveGenerationConfig.Internal?, + val tools: List?, + val systemInstruction: Content.Internal? + ) + } + + fun toInternal() = + Internal(Internal.BidiGenerateContentSetup(model, generationConfig, tools, systemInstruction)) +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt index dd928f92273..ecd4e74d80a 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ContentModality.kt @@ -46,6 +46,15 @@ public class ContentModality private constructor(public val ordinal: Int) { } } + internal fun toInternal() = + when (this) { + TEXT -> "TEXT" + IMAGE -> "IMAGE" + VIDEO -> "VIDEO" + AUDIO -> "AUDIO" + DOCUMENT -> "DOCUMENT" + else -> "UNSPECIFIED" + } public companion object { /** Unspecified modality. */ @JvmField public val UNSPECIFIED: ContentModality = ContentModality(0) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt index 4a29e5c37ea..f3256bf4c15 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Exceptions.kt @@ -175,6 +175,20 @@ public class QuotaExceededException internal constructor(message: String, cause: Throwable? = null) : FirebaseVertexAIException(message, cause) +/** Streaming session already receiving. */ +public class SessionAlreadyReceivingException : + FirebaseVertexAIException( + "This session is already receiving. Please call stopReceiving() before calling this again." + ) + +/** Audio record initialization failures for audio streaming */ +public class AudioRecordInitializationFailedException(message: String) : + FirebaseVertexAIException(message) + +/** Handshake failed with the server */ +public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) : + FirebaseVertexAIException(message, cause) + /** Catch all case for exceptions not explicitly expected. */ public class UnknownException internal constructor(message: String, cause: Throwable? = null) : FirebaseVertexAIException(message, cause) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveContentResponse.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveContentResponse.kt new file mode 100644 index 00000000000..96021745d1d --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveContentResponse.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +/* Represents the response from the server. */ +@PublicPreviewAPI +public class LiveContentResponse +internal constructor( + public val data: Content?, + public val status: Status, + public val functionCalls: List? +) { + /** + * Convenience field representing all the text parts in the response as a single string, if they + * exists. + */ + public val text: String? = + data?.parts?.filterIsInstance()?.joinToString(" ") { it.text } + + @JvmInline + public value class Status private constructor(private val value: Int) { + public companion object { + public val NORMAL: Status = Status(0) + public val INTERRUPTED: Status = Status(1) + public val TURN_COMPLETE: Status = Status(2) + } + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveGenerationConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveGenerationConfig.kt new file mode 100644 index 00000000000..55e789fd14f --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveGenerationConfig.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Configuration parameters to use for content generation. + * + * @property temperature A parameter controlling the degree of randomness in token selection. A + * temperature of 0 means that the highest probability tokens are always selected. In this case, + * responses for a given prompt are mostly deterministic, but a small amount of variation is still + * possible. + * + * @property topK The `topK` parameter changes how the model selects tokens for output. A `topK` of + * 1 means the selected token is the most probable among all the tokens in the model's vocabulary, + * while a `topK` of 3 means that the next token is selected from among the 3 most probable using + * the `temperature`. For each token selection step, the `topK` tokens with the highest + * probabilities are sampled. Tokens are then further filtered based on `topP` with the final token + * selected using `temperature` sampling. Defaults to 40 if unspecified. + * + * @property topP The `topP` parameter changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the topP value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + * + * @property candidateCount The maximum number of generated response messages to return. This value + * must be between [1, 8], inclusive. If unset, this will default to 1. + * + * - Note: Only unique candidates are returned. Higher temperatures are more likely to produce + * unique candidates. Setting `temperature` to 0 will always produce exactly one candidate + * regardless of the `candidateCount`. + * + * @property presencePenalty Positive penalties. + * + * @property frequencyPenalty Frequency penalties. + * + * @property maxOutputTokens Specifies the maximum number of tokens that can be generated in the + * response. The number of tokens per word varies depending on the language outputted. Defaults to 0 + * (unbounded). + * + * @property responseModality Specifies the format of the data in which the server responds to + * requests + * + * @property speechConfig Specifies the voice configuration of the audio response from the server. + * + * Refer to the + * [Control generated output](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) + * guide for more details. + */ +@PublicPreviewAPI +public class LiveGenerationConfig +private constructor( + internal val temperature: Float?, + internal val topK: Int?, + internal val topP: Float?, + internal val candidateCount: Int?, + internal val maxOutputTokens: Int?, + internal val presencePenalty: Float?, + internal val frequencyPenalty: Float?, + internal val responseModality: ResponseModality?, + internal val speechConfig: SpeechConfig? +) { + + /** + * Builder for creating a [LiveGenerationConfig]. + * + * Mainly intended for Java interop. Kotlin consumers should use [liveGenerationConfig] for a more + * idiomatic experience. + * + * @property temperature See [LiveGenerationConfig.temperature]. + * + * @property topK See [LiveGenerationConfig.topK]. + * + * @property topP See [LiveGenerationConfig.topP]. + * + * @property presencePenalty See [LiveGenerationConfig.presencePenalty] + * + * @property frequencyPenalty See [LiveGenerationConfig.frequencyPenalty] + * + * @property candidateCount See [LiveGenerationConfig.candidateCount]. + * + * @property maxOutputTokens See [LiveGenerationConfig.maxOutputTokens]. + * + * @property responseModality See [LiveGenerationConfig.responseModality] + * + * @property speechConfig See [LiveGenerationConfig.speechConfig] + */ + public class Builder { + @JvmField public var temperature: Float? = null + @JvmField public var topK: Int? = null + @JvmField public var topP: Float? = null + @JvmField public var candidateCount: Int? = null + @JvmField public var maxOutputTokens: Int? = null + @JvmField public var presencePenalty: Float? = null + @JvmField public var frequencyPenalty: Float? = null + @JvmField public var responseModality: ResponseModality? = null + @JvmField public var speechConfig: SpeechConfig? = null + + public fun setTemperature(temperature: Float?): Builder = apply { + this.temperature = temperature + } + public fun setTopK(topK: Int?): Builder = apply { this.topK = topK } + public fun setTopP(topP: Float?): Builder = apply { this.topP = topP } + public fun setCandidateCount(candidateCount: Int?): Builder = apply { + this.candidateCount = candidateCount + } + public fun setMaxOutputTokens(maxOutputTokens: Int?): Builder = apply { + this.maxOutputTokens = maxOutputTokens + } + public fun setPresencePenalty(presencePenalty: Float?): Builder = apply { + this.presencePenalty = presencePenalty + } + public fun setFrequencyPenalty(frequencyPenalty: Float?): Builder = apply { + this.frequencyPenalty = frequencyPenalty + } + public fun setResponseModalities(responseModalities: ResponseModality?): Builder = apply { + this.responseModality = responseModalities + } + public fun setSpeechConfig(speechConfig: SpeechConfig?): Builder = apply { + this.speechConfig = speechConfig + } + + /** Create a new [LiveGenerationConfig] with the attached arguments. */ + public fun build(): LiveGenerationConfig = + LiveGenerationConfig( + temperature = temperature, + topK = topK, + topP = topP, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + presencePenalty = presencePenalty, + frequencyPenalty = frequencyPenalty, + speechConfig = speechConfig, + responseModality = responseModality + ) + } + + internal fun toInternal(): Internal { + return Internal( + temperature = temperature, + topP = topP, + topK = topK, + candidateCount = candidateCount, + maxOutputTokens = maxOutputTokens, + frequencyPenalty = frequencyPenalty, + presencePenalty = presencePenalty, + speechConfig = speechConfig?.toInternal(), + responseModalities = + if (responseModality != null) listOf(responseModality.toInternal()) else null + ) + } + + @Serializable + internal data class Internal( + val temperature: Float?, + @SerialName("top_p") val topP: Float?, + @SerialName("top_k") val topK: Int?, + @SerialName("candidate_count") val candidateCount: Int?, + @SerialName("max_output_tokens") val maxOutputTokens: Int?, + @SerialName("presence_penalty") val presencePenalty: Float? = null, + @SerialName("frequency_penalty") val frequencyPenalty: Float? = null, + @SerialName("speech_config") val speechConfig: SpeechConfig.Internal? = null, + @SerialName("response_modalities") val responseModalities: List? = null + ) + + public companion object { + + /** + * Alternative casing for [LiveGenerationConfig.Builder]: + * ``` + * val config = LiveGenerationConfig.builder() + * ``` + */ + public fun builder(): Builder = Builder() + } +} + +/** + * Helper method to construct a [LiveGenerationConfig] in a DSL-like manner. + * + * Example Usage: + * ``` + * liveGenerationConfig { + * temperature = 0.75f + * topP = 0.5f + * topK = 30 + * candidateCount = 4 + * maxOutputTokens = 300 + * ... + * } + * ``` + */ +@OptIn(PublicPreviewAPI::class) +public fun liveGenerationConfig( + init: LiveGenerationConfig.Builder.() -> Unit +): LiveGenerationConfig { + val builder = LiveGenerationConfig.builder() + builder.init() + return builder.build() +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveSession.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveSession.kt new file mode 100644 index 00000000000..b3bdae1f707 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/LiveSession.kt @@ -0,0 +1,358 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import android.media.AudioFormat +import android.media.AudioTrack +import android.util.Log +import com.google.firebase.annotations.concurrent.Background +import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readBytes +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNull + +/** Represents a live WebSocket session capable of streaming content to and from the server. */ +@PublicPreviewAPI +@OptIn(ExperimentalSerializationApi::class) +public class LiveSession +internal constructor( + private val session: ClientWebSocketSession?, + @Background private val backgroundDispatcher: CoroutineContext, + private var audioHelper: AudioHelper? = null +) { + + private val audioQueue = ConcurrentLinkedQueue() + private val playBackQueue = ConcurrentLinkedQueue() + private var startedReceiving = false + private var receiveChannel: Channel = Channel() + private var isRecording: Boolean = false + + private companion object { + val TAG = LiveSession::class.java.simpleName + val MIN_BUFFER_SIZE = + AudioTrack.getMinBufferSize( + 24000, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + } + + internal class ClientContentSetup(val turns: List, val turnComplete: Boolean) { + @Serializable + internal class Internal(@SerialName("client_content") val clientContent: ClientContent) { + @Serializable + internal data class ClientContent( + val turns: List, + @SerialName("turn_complete") val turnComplete: Boolean + ) + } + + fun toInternal() = Internal(Internal.ClientContent(turns, turnComplete)) + } + + @OptIn(ExperimentalSerializationApi::class) + internal class ToolResponseSetup( + val functionResponses: List + ) { + + @Serializable + internal data class Internal(val toolResponse: ToolResponse) { + @Serializable + internal data class ToolResponse( + val functionResponses: List + ) + } + + fun toInternal() = Internal(Internal.ToolResponse(functionResponses)) + } + + internal class ServerContentSetup(val modelTurn: Content.Internal) { + @Serializable + internal class Internal(@SerialName("serverContent") val serverContent: ServerContent) { + @Serializable + internal data class ServerContent(@SerialName("modelTurn") val modelTurn: Content.Internal) + } + + fun toInternal() = Internal(Internal.ServerContent(modelTurn)) + } + + internal class MediaStreamingSetup(val mediaChunks: List) { + @Serializable + internal class Internal(val realtimeInput: MediaChunks) { + @Serializable internal data class MediaChunks(val mediaChunks: List) + } + fun toInternal() = Internal(Internal.MediaChunks(mediaChunks)) + } + + internal data class ToolCallSetup( + val functionCalls: List + ) { + + @Serializable + internal class Internal(val toolCall: ToolCall) { + + @Serializable + internal data class ToolCall(val functionCalls: List) + } + + fun toInternal(): Internal { + return Internal(Internal.ToolCall(functionCalls)) + } + } + + private fun fillRecordedAudioQueue() { + CoroutineScope(backgroundDispatcher).launch { + audioHelper!!.startRecording().collect { + if (!isRecording) { + cancel() + } + audioQueue.add(it) + } + } + } + + private suspend fun sendAudioDataToServer() { + var offset = 0 + val audioBuffer = ByteArray(MIN_BUFFER_SIZE * 2) + while (isRecording) { + val receivedAudio = audioQueue.poll() ?: continue + receivedAudio.copyInto(audioBuffer, offset) + offset += receivedAudio.size + if (offset >= MIN_BUFFER_SIZE) { + sendMediaStream(listOf(MediaData(audioBuffer, "audio/pcm"))) + audioBuffer.fill(0) + offset = 0 + } + } + } + + private fun fillServerResponseAudioQueue( + functionCallsHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null + ) { + CoroutineScope(backgroundDispatcher).launch { + receive().collect { + if (!isRecording) { + cancel() + } + when (it.status) { + LiveContentResponse.Status.INTERRUPTED -> + while (!playBackQueue.isEmpty()) playBackQueue.poll() + LiveContentResponse.Status.NORMAL -> + if (!it.functionCalls.isNullOrEmpty() && functionCallsHandler != null) { + sendFunctionResponse(it.functionCalls.map(functionCallsHandler).toList()) + } else { + val audioData = it.data?.parts?.get(0)?.asInlineDataPartOrNull()?.inlineData + if (audioData != null) { + playBackQueue.add(audioData) + } + } + } + } + } + } + + private fun playServerResponseAudio() { + CoroutineScope(backgroundDispatcher).launch { + while (isRecording) { + val x = playBackQueue.poll() ?: continue + audioHelper?.playAudio(x) + } + } + } + + /** + * Starts an audio conversation with the Gemini server, which can only be stopped using + * [stopAudioConversation]. + * + * @param functionCallHandler A callback function that is invoked whenever the server receives a + * function call. + */ + public suspend fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null + ) { + if (isRecording) { + Log.w(TAG, "startAudioConversation called after the recording has already started.") + return + } + isRecording = true + audioHelper = AudioHelper() + audioHelper!!.setupAudioTrack() + fillRecordedAudioQueue() + CoroutineScope(backgroundDispatcher).launch { sendAudioDataToServer() } + fillServerResponseAudioQueue(functionCallHandler) + playServerResponseAudio() + } + + /** + * Stops the audio conversation with the Gemini Server. This needs to be called only after calling + * [startAudioConversation] + */ + public fun stopAudioConversation() { + stopReceiving() + isRecording = false + audioHelper?.let { + while (playBackQueue.isNotEmpty()) playBackQueue.poll() + while (audioQueue.isNotEmpty()) audioQueue.poll() + it.release() + } + audioHelper = null + } + + /** + * Stops receiving from the server. If this function is called during an ongoing audio + * conversation, the server's response will not be received, and no audio will be played. + */ + public fun stopReceiving() { + if (!startedReceiving) { + return + } + receiveChannel.cancel() + receiveChannel = Channel() + startedReceiving = false + } + + /** + * Receives responses from the server for both streaming and standard requests. Call + * [stopReceiving] to stop receiving responses from the server. + * + * @return A [Flow] which will emit [LiveContentResponse] as and when it receives it + * + * @throws [SessionAlreadyReceivingException] when the session is already receiving. + */ + public fun receive(): Flow { + if (startedReceiving) { + throw SessionAlreadyReceivingException() + } + + val flowReceive = session!!.incoming.receiveAsFlow() + CoroutineScope(backgroundDispatcher).launch { flowReceive.collect { receiveChannel.send(it) } } + return flow { + startedReceiving = true + while (true) { + val message = receiveChannel.receive() + val receivedBytes = (message as Frame.Binary).readBytes() + val receivedJson = receivedBytes.toString(Charsets.UTF_8) + if (receivedJson.contains("interrupted")) { + emit(LiveContentResponse(null, LiveContentResponse.Status.INTERRUPTED, null)) + continue + } + if (receivedJson.contains("turnComplete")) { + emit(LiveContentResponse(null, LiveContentResponse.Status.TURN_COMPLETE, null)) + continue + } + try { + val serverContent = Json.decodeFromString(receivedJson) + val data = serverContent.serverContent.modelTurn.toPublic() + if (data.parts[0].asInlineDataPartOrNull()?.mimeType?.equals("audio/pcm") == true) { + emit(LiveContentResponse(data, LiveContentResponse.Status.NORMAL, null)) + } + if (data.parts[0] is TextPart) { + emit(LiveContentResponse(data, LiveContentResponse.Status.NORMAL, null)) + } + continue + } catch (e: Exception) { + Log.i(TAG, "Failed to decode server content: ${e.message}") + } + try { + val functionContent = Json.decodeFromString(receivedJson) + emit( + LiveContentResponse( + null, + LiveContentResponse.Status.NORMAL, + functionContent.toolCall.functionCalls.map { + FunctionCallPart(it.name, it.args.orEmpty().mapValues { x -> x.value ?: JsonNull }) + } + ) + ) + continue + } catch (e: Exception) { + Log.w(TAG, "Failed to decode function calling: ${e.message}") + } + } + } + } + + /** + * Sends the function calling responses to the server. + * + * @param functionList The list of [FunctionResponsePart] instances indicating the function + * response from the client. + */ + public suspend fun sendFunctionResponse(functionList: List) { + val jsonString = + Json.encodeToString( + ToolResponseSetup(functionList.map { it.toInternalFunctionCall() }).toInternal() + ) + session?.send(Frame.Text(jsonString)) + } + + /** + * Streams client data to the server. Calling this after [startAudioConversation] will play the + * response audio immediately. + * + * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. + */ + public suspend fun sendMediaStream( + mediaChunks: List, + ) { + val jsonString = + Json.encodeToString(MediaStreamingSetup(mediaChunks.map { it.toInternal() }).toInternal()) + session?.send(Frame.Text(jsonString)) + } + + /** + * Sends data to the server. Calling this after [startAudioConversation] will play the response + * audio immediately. + * + * @param content Client [Content] to be sent to the server. + */ + public suspend fun send(content: Content) { + val jsonString = + Json.encodeToString(ClientContentSetup(listOf(content.toInternal()), true).toInternal()) + session?.send(Frame.Text(jsonString)) + } + + /** + * Sends text to the server. Calling this after [startAudioConversation] will play the response + * audio immediately. + * + * @param text Text to be sent to the server. + */ + public suspend fun send(text: String) { + send(Content.Builder().text(text).build()) + } + + /** Closes the client session. */ + public suspend fun close() { + session?.close() + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/MediaData.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/MediaData.kt new file mode 100644 index 00000000000..7e58c9cf43c --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/MediaData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import android.util.Base64 +import kotlinx.serialization.Serializable + +/** + * Represents the media data to be sent to the server + * + * @param data Byte array representing the data to be sent. + * @param mimeType an IANA standard MIME type. For supported MIME type values see the + * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). + */ +@PublicPreviewAPI +public class MediaData(public val data: ByteArray, public val mimeType: String) { + @Serializable + internal class Internal( + val data: String, + val mimeType: String, + ) + + internal fun toInternal(): Internal { + return Internal(Base64.encodeToString(data, BASE_64_FLAGS), mimeType) + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt index a0a47cf79ee..21d3c0edc6c 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Part.kt @@ -57,11 +57,11 @@ public class ImagePart(public val image: Bitmap) : Part public class InlineDataPart(public val inlineData: ByteArray, public val mimeType: String) : Part { @Serializable - internal data class Internal(@SerialName("inline_data") val inlineData: InlineData) : + internal data class Internal(@SerialName("inlineData") val inlineData: InlineData) : InternalPart { @Serializable - internal data class InlineData(@SerialName("mime_type") val mimeType: String, val data: Base64) + internal data class InlineData(@SerialName("mimeType") val mimeType: String, val data: Base64) } } @@ -95,6 +95,10 @@ public class FunctionResponsePart(public val name: String, public val response: @Serializable internal data class FunctionResponse(val name: String, val response: JsonObject) } + + internal fun toInternalFunctionCall(): Internal.FunctionResponse { + return Internal.FunctionResponse(this.name, this.response) + } } /** diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ResponseModality.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ResponseModality.kt new file mode 100644 index 00000000000..e8fe70db157 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ResponseModality.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import com.google.firebase.vertexai.common.util.FirstOrdinalSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Modality for bidirectional streaming. */ +@PublicPreviewAPI +public class ResponseModality private constructor(public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + @SerialName("MODALITY_UNSPECIFIED") UNSPECIFIED, + TEXT, + IMAGE, + AUDIO; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + TEXT -> ResponseModality.TEXT + IMAGE -> ResponseModality.IMAGE + AUDIO -> ResponseModality.AUDIO + else -> ResponseModality.UNSPECIFIED + } + } + + internal fun toInternal() = + when (this) { + TEXT -> "TEXT" + IMAGE -> "IMAGE" + AUDIO -> "AUDIO" + else -> "UNSPECIFIED" + } + public companion object { + /** Unspecified modality. */ + @JvmField public val UNSPECIFIED: ResponseModality = ResponseModality(0) + + /** Plain text. */ + @JvmField public val TEXT: ResponseModality = ResponseModality(1) + + /** Image. */ + @JvmField public val IMAGE: ResponseModality = ResponseModality(2) + + /** Audio. */ + @JvmField public val AUDIO: ResponseModality = ResponseModality(4) + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/SpeechConfig.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/SpeechConfig.kt new file mode 100644 index 00000000000..c304bb6a60e --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/SpeechConfig.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Speech configuration class for setting up the voice of the server's response. */ +@PublicPreviewAPI +public class SpeechConfig(public val voice: Voices) { + + @Serializable + internal data class Internal(@SerialName("voice_config") val voiceConfig: VoiceConfigInternal) { + @Serializable + internal data class VoiceConfigInternal( + @SerialName("prebuilt_voice_config") val prebuiltVoiceConfig: Voices.Internal, + ) + } + + internal fun toInternal(): Internal { + return Internal(Internal.VoiceConfigInternal(prebuiltVoiceConfig = voice.toInternal())) + } +} diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Voices.kt b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Voices.kt new file mode 100644 index 00000000000..a9ca6390489 --- /dev/null +++ b/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/Voices.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.vertexai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Various voices supported by the server */ +@PublicPreviewAPI +public class Voices private constructor(public val ordinal: Int) { + + @Serializable internal data class Internal(@SerialName("voice_name") val voiceName: String) + + @Serializable + internal enum class InternalEnum { + CHARON, + AOEDE, + FENRIR, + KORE, + PUCK; + internal fun toPublic() = + when (this) { + CHARON -> Voices.CHARON + AOEDE -> Voices.AOEDE + FENRIR -> Voices.FENRIR + KORE -> Voices.KORE + else -> Voices.PUCK + } + } + + internal fun toInternal(): Internal { + return when (this) { + CHARON -> Internal(InternalEnum.CHARON.name) + AOEDE -> Internal(InternalEnum.AOEDE.name) + FENRIR -> Internal(InternalEnum.FENRIR.name) + KORE -> Internal(InternalEnum.KORE.name) + else -> Internal(InternalEnum.PUCK.name) + } + } + + public companion object { + /** Unspecified modality. */ + @JvmField public val UNSPECIFIED: Voices = Voices(0) + + @JvmField public val CHARON: Voices = Voices(1) + + @JvmField public val AOEDE: Voices = Voices(2) + + @JvmField public val FENRIR: Voices = Voices(3) + + @JvmField public val KORE: Voices = Voices(4) + + @JvmField public val PUCK: Voices = Voices(5) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c94f0378b2..44079b349e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -153,6 +153,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVer ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorVersion" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorVersion" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorVersion" } +ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorVersion" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" } material = { module = "com.google.android.material:material", version.ref = "material" } maven-resolver-api = { module = "org.apache.maven.resolver:maven-resolver-api", version.ref = "mavenResolverApi" } From 278e437a7dd02c9046186eda707a020e63e214d4 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande <4131300+tejasd@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:32:42 -0400 Subject: [PATCH 075/162] Changes in the Session Test App to verify behaviour with Fireperf #no-changelog (#6809) This change adds a way to repeatedly log identical performance traces in different processes. - Currently traces on different activities are logged on different Fireperf Sessions. - Adds additional logging to help identify different Firebase instances in different processes. - Adds Fireperf custom attributes to identify processes. --- .../sessions/SessionLifecycleService.kt | 1 + .../firebase/testing/sessions/BaseActivity.kt | 28 ++++++++++++++++++- .../testing/sessions/FirstFragment.kt | 20 +++++++++++++ .../testing/sessions/SecondActivity.kt | 15 +++++++++- .../src/main/res/layout/activity_second.xml | 14 ++++++++++ .../src/main/res/layout/fragment_first.xml | 14 ++++++++++ .../test-app/src/main/res/values/strings.xml | 1 + .../test-app/test-app.gradle.kts | 2 ++ 8 files changed, 93 insertions(+), 2 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index 6807d8bec69..85930dc5455 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -193,6 +193,7 @@ internal class SessionLifecycleService : Service() { handlerThread.start() messageHandler = MessageHandler(handlerThread.looper) messenger = Messenger(messageHandler) + Log.d(TAG, "Service created on process ${android.os.Process.myPid()}") } /** Called when a new [SessionLifecycleClient] binds to this service. */ diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt index 8c2670696aa..8163abbccf2 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt @@ -24,12 +24,16 @@ import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.google.firebase.FirebaseApp +import com.google.firebase.perf.FirebasePerformance open class BaseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FirebaseApp.initializeApp(this) + setProcessAttribute() + logProcessDetails() + logFirebaseDetails() Log.i(TAG, "onCreate - ${getProcessName()} - ${getImportance()}") } @@ -64,9 +68,31 @@ open class BaseActivity : AppCompatActivity() { return processInfo.importance } - private fun getProcessName(): String = + protected fun getProcessName(): String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) Application.getProcessName() else "unknown" + private fun logProcessDetails() { + val pid = android.os.Process.myPid() + val uid = android.os.Process.myUid() + val activity = javaClass.name + val process = getProcessName() + Log.i(TAG, "activity: $activity process: $process, pid: $pid, uid: $uid") + } + + private fun logFirebaseDetails() { + val activity = javaClass.name + val firebaseApps = FirebaseApp.getApps(this) + val defaultFirebaseApp = FirebaseApp.getInstance() + Log.i( + TAG, + "activity: $activity firebase: ${defaultFirebaseApp.name} appsCount: ${firebaseApps.count()}" + ) + } + + private fun setProcessAttribute() { + FirebasePerformance.getInstance().putAttribute("process_name", getProcessName()) + } + companion object { val TAG = "BaseActivity" } diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt index 88488a4cc92..f5a965da7d4 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt @@ -16,6 +16,7 @@ package com.google.firebase.testing.sessions +import android.app.Application import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -26,14 +27,20 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.perf.FirebasePerformance import com.google.firebase.testing.sessions.databinding.FragmentFirstBinding import java.util.Date import java.util.Locale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** A simple [Fragment] subclass as the default destination in the navigation. */ class FirstFragment : Fragment() { val crashlytics = FirebaseCrashlytics.getInstance() + val performance = FirebasePerformance.getInstance() private var _binding: FragmentFirstBinding? = null @@ -64,6 +71,14 @@ class FirstFragment : Fragment() { Thread.sleep(1_000) } } + binding.createTrace.setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + val performanceTrace = performance.newTrace("test_trace") + performanceTrace.start() + delay(1000) + performanceTrace.stop() + } + } binding.buttonForegroundProcess.setOnClickListener { if (binding.buttonForegroundProcess.getText().startsWith("Start")) { ForegroundService.startService(requireContext(), "Starting service at ${getDateText()}") @@ -89,6 +104,7 @@ class FirstFragment : Fragment() { intent.addFlags(FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } + binding.processName.text = getProcessName() } override fun onResume() { @@ -111,5 +127,9 @@ class FirstFragment : Fragment() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) else "unknown" + + fun getProcessName(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) Application.getProcessName() + else "unknown" } } diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt index 9272510d0f3..6c2fd3c06b0 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt @@ -22,10 +22,14 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Build import android.os.Bundle import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.lifecycleScope +import com.google.firebase.perf.FirebasePerformance +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** Second activity from the MainActivity that runs on a different process. */ class SecondActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) @@ -38,12 +42,21 @@ class SecondActivity : BaseActivity() { findViewById