From 584742fb6a080a98fb0a2ac5e332b253a0d0b263 Mon Sep 17 00:00:00 2001 From: Ashish Kothari Date: Thu, 5 Feb 2026 10:52:06 +0530 Subject: [PATCH 01/11] feat(rc): Support Remote Config managed value types (#1167) * Support Remote Config managed value types Update Remote Config API to support managed value types Added support for Rollouts, Experiments, and Personalization value types in the Remote Config API. - Updated `ParameterValue` to include `RolloutValue`, `PersonalizationValue`, and `ExperimentValue` subclasses. - Added factory methods `ofRollout`, `ofPersonalization`, and `ofExperiment` to `ParameterValue`. - Updated `TemplateResponse` to parse `rolloutValue`, `personalizationValue`, and `experimentValue` fields from the backend response. - Added unit tests for the new value types. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../firebase/remoteconfig/ParameterValue.java | 286 ++++++++++++++++++ .../internal/TemplateResponse.java | 170 +++++++++++ .../FirebaseRemoteConfigTest.java | 30 ++ .../firebase/remoteconfig/ParameterTest.java | 18 ++ .../remoteconfig/ParameterValueTest.java | 83 +++++ 5 files changed, 587 insertions(+) diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java index 90bf0e5df..7bf9d3ba0 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java +++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java @@ -17,10 +17,18 @@ package com.google.firebase.remoteconfig; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ExperimentValueResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ExperimentVariantValueResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.PersonalizationValueResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.RolloutValueResponse; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -47,6 +55,40 @@ public static InAppDefault inAppDefault() { return new InAppDefault(); } + /** + * Creates a new {@link ParameterValue.RolloutValue} instance. + * + * @param rolloutId The rollout ID. + * @param value The value of the rollout. + * @param percent The percentage of the rollout. + * @return A {@link ParameterValue.RolloutValue} instance. + */ + public static RolloutValue ofRollout(String rolloutId, String value, double percent) { + return new RolloutValue(rolloutId, value, percent); + } + + /** + * Creates a new {@link ParameterValue.PersonalizationValue} instance. + * + * @param personalizationId The personalization ID. + * @return A {@link ParameterValue.PersonalizationValue} instance. + */ + public static PersonalizationValue ofPersonalization(String personalizationId) { + return new PersonalizationValue(personalizationId); + } + + /** + * Creates a new {@link ParameterValue.ExperimentValue} instance. + * + * @param experimentId The experiment ID. + * @param variantValues The list of experiment variant values. + * @return A {@link ParameterValue.ExperimentValue} instance. + */ + public static ExperimentValue ofExperiment(String experimentId, + List variantValues) { + return new ExperimentValue(experimentId, variantValues); + } + abstract ParameterValueResponse toParameterValueResponse(); static ParameterValue fromParameterValueResponse( @@ -55,6 +97,30 @@ static ParameterValue fromParameterValueResponse( if (parameterValueResponse.isUseInAppDefault()) { return ParameterValue.inAppDefault(); } + if (parameterValueResponse.getRolloutValue() != null) { + RolloutValueResponse rv = parameterValueResponse.getRolloutValue(); + // Protobuf serialization does not set values for fields on the wire when + // they are equal to the default value for the field type. When deserializing, + // can appear as the value not being set. Explicitly handle default value for + // the percent field since 0 is a valid value. + double percent = 0; + if (rv.getPercent() != null) { + percent = rv.getPercent(); + } + return ParameterValue.ofRollout(rv.getRolloutId(), rv.getValue(), percent); + } + if (parameterValueResponse.getPersonalizationValue() != null) { + PersonalizationValueResponse pv = parameterValueResponse.getPersonalizationValue(); + return ParameterValue.ofPersonalization(pv.getPersonalizationId()); + } + if (parameterValueResponse.getExperimentValue() != null) { + ExperimentValueResponse ev = parameterValueResponse.getExperimentValue(); + List variantValues = ev.getExperimentVariantValues().stream() + .map(evv -> new ExperimentVariantValue( + evv.getVariantId(), evv.getValue(), evv.getNoChange())) + .collect(toList()); + return ParameterValue.ofExperiment(ev.getExperimentId(), variantValues); + } return ParameterValue.of(parameterValueResponse.getValue()); } @@ -124,4 +190,224 @@ public boolean equals(Object o) { return true; } } + + /** + * Represents a Rollout value. + */ + public static final class RolloutValue extends ParameterValue { + private final String rolloutId; + private final String value; + private final double percent; + + private RolloutValue(String rolloutId, String value, double percent) { + this.rolloutId = rolloutId; + this.value = value; + this.percent = percent; + } + + public String getRolloutId() { + return rolloutId; + } + + public String getValue() { + return value; + } + + public double getPercent() { + return percent; + } + + @Override + ParameterValueResponse toParameterValueResponse() { + return new ParameterValueResponse().setRolloutValue( + new RolloutValueResponse() + .setRolloutId(this.rolloutId) + .setValue(this.value) + .setPercent(this.percent)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RolloutValue that = (RolloutValue) o; + return Double.compare(that.percent, percent) == 0 + && Objects.equals(rolloutId, that.rolloutId) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(rolloutId, value, percent); + } + } + + /** + * Represents a Personalization value. + */ + public static final class PersonalizationValue extends ParameterValue { + private final String personalizationId; + + private PersonalizationValue(String personalizationId) { + this.personalizationId = personalizationId; + } + + public String getPersonalizationId() { + return personalizationId; + } + + @Override + ParameterValueResponse toParameterValueResponse() { + return new ParameterValueResponse().setPersonalizationValue( + new PersonalizationValueResponse() + .setPersonalizationId(this.personalizationId)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersonalizationValue that = (PersonalizationValue) o; + return Objects.equals(personalizationId, that.personalizationId); + } + + @Override + public int hashCode() { + return Objects.hash(personalizationId); + } + } + + /** + * Represents a specific variant within an Experiment. + */ + public static final class ExperimentVariantValue { + private final String variantId; + private final String value; + private final Boolean noChange; + + ExperimentVariantValue(String variantId, String value, Boolean noChange) { + this.variantId = variantId; + this.value = value; + this.noChange = noChange; + } + + /** + * Creates a new {@link ExperimentVariantValue} instance. + * + * @param variantId The variant ID. + * @param value The value of the variant. + * @return A {@link ExperimentVariantValue} instance. + */ + public static ExperimentVariantValue of(String variantId, String value) { + return new ExperimentVariantValue(variantId, value, null); + } + + /** + * Creates a new {@link ExperimentVariantValue} instance. + * + * @param variantId The variant ID. + * @return A {@link ExperimentVariantValue} instance. + */ + public static ExperimentVariantValue ofNoChange(String variantId) { + return new ExperimentVariantValue(variantId, null, true); + } + + public String getVariantId() { + return variantId; + } + + @Nullable + public String getValue() { + return value; + } + + @Nullable + Boolean getNoChange() { + return noChange; + } + + public boolean isNoChange() { + return Boolean.TRUE.equals(noChange); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExperimentVariantValue that = (ExperimentVariantValue) o; + return noChange == that.noChange + && Objects.equals(variantId, that.variantId) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(variantId, value, noChange); + } + } + + /** + * Represents an Experiment value. + */ + public static final class ExperimentValue extends ParameterValue { + private final String experimentId; + private final List variantValues; + + private ExperimentValue(String experimentId, List variantValues) { + this.experimentId = experimentId; + this.variantValues = variantValues; + } + + public String getExperimentId() { + return experimentId; + } + + public List getExperimentVariantValues() { + return variantValues; + } + + @Override + ParameterValueResponse toParameterValueResponse() { + List variantValueResponses = variantValues.stream() + .map(variantValue -> new ExperimentVariantValueResponse() + .setVariantId(variantValue.getVariantId()) + .setValue(variantValue.getValue()) + .setNoChange(variantValue.getNoChange())) + .collect(toList()); + return new ParameterValueResponse().setExperimentValue( + new ExperimentValueResponse() + .setExperimentId(this.experimentId) + .setExperimentVariantValues(variantValueResponses)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExperimentValue that = (ExperimentValue) o; + return Objects.equals(experimentId, that.experimentId) + && Objects.equals(variantValues, that.variantValues); + } + + @Override + public int hashCode() { + return Objects.hash(experimentId, variantValues); + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index 57a066bf2..b92580f8c 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -161,6 +161,15 @@ public static final class ParameterValueResponse { @Key("useInAppDefault") private Boolean useInAppDefault; + @Key("rolloutValue") + private RolloutValueResponse rolloutValue; + + @Key("personalizationValue") + private PersonalizationValueResponse personalizationValue; + + @Key("experimentValue") + private ExperimentValueResponse experimentValue; + public String getValue() { return value; } @@ -169,6 +178,18 @@ public boolean isUseInAppDefault() { return Boolean.TRUE.equals(this.useInAppDefault); } + public RolloutValueResponse getRolloutValue() { + return rolloutValue; + } + + public PersonalizationValueResponse getPersonalizationValue() { + return personalizationValue; + } + + public ExperimentValueResponse getExperimentValue() { + return experimentValue; + } + public ParameterValueResponse setValue(String value) { this.value = value; return this; @@ -178,6 +199,155 @@ public ParameterValueResponse setUseInAppDefault(boolean useInAppDefault) { this.useInAppDefault = useInAppDefault; return this; } + + public ParameterValueResponse setRolloutValue(RolloutValueResponse rolloutValue) { + this.rolloutValue = rolloutValue; + return this; + } + + public ParameterValueResponse setPersonalizationValue( + PersonalizationValueResponse personalizationValue) { + this.personalizationValue = personalizationValue; + return this; + } + + public ParameterValueResponse setExperimentValue(ExperimentValueResponse experimentValue) { + this.experimentValue = experimentValue; + return this; + } + } + + /** + * The Data Transfer Object for parsing Remote Config Rollout value responses from the + * Remote Config service. + **/ + public static final class RolloutValueResponse { + @Key("rolloutId") + private String rolloutId; + + @Key("value") + private String value; + + @Key("percent") + private Double percent; + + public String getRolloutId() { + return rolloutId; + } + + public String getValue() { + return value; + } + + public Double getPercent() { + return percent; + } + + public RolloutValueResponse setRolloutId(String rolloutId) { + this.rolloutId = rolloutId; + return this; + } + + public RolloutValueResponse setValue(String value) { + this.value = value; + return this; + } + + public RolloutValueResponse setPercent(Double percent) { + this.percent = percent; + return this; + } + } + + /** + * The Data Transfer Object for parsing Remote Config Personalization value responses from the + * Remote Config service. + **/ + public static final class PersonalizationValueResponse { + @Key("personalizationId") + private String personalizationId; + + public String getPersonalizationId() { + return personalizationId; + } + + public PersonalizationValueResponse setPersonalizationId(String personalizationId) { + this.personalizationId = personalizationId; + return this; + } + } + + /** + * The Data Transfer Object for parsing Remote Config Experiment value responses from the + * Remote Config service. + **/ + public static final class ExperimentValueResponse { + @Key("experimentId") + private String experimentId; + + @Key("variantValue") + private List experimentVariantValues; + + public String getExperimentId() { + return experimentId; + } + + public List getExperimentVariantValues() { + return experimentVariantValues; + } + + public ExperimentValueResponse setExperimentId(String experimentId) { + this.experimentId = experimentId; + return this; + } + + public ExperimentValueResponse setExperimentVariantValues( + List experimentVariantValues) { + this.experimentVariantValues = experimentVariantValues; + return this; + } + } + + /** + * The Data Transfer Object for parsing Remote Config Experiment variant value responses from the + * Remote Config service. + **/ + public static final class ExperimentVariantValueResponse { + @Key("variantId") + private String variantId; + + @Key("value") + private String value; + + @Key("noChange") + private Boolean noChange; + + public String getVariantId() { + return variantId; + } + + public String getValue() { + return value; + } + + public Boolean getNoChange() { + return noChange; + } + + public ExperimentVariantValueResponse setVariantId(String variantId) { + this.variantId = variantId; + return this; + } + + public ExperimentVariantValueResponse setValue(String value) { + this.value = value; + return this; + } + + public ExperimentVariantValueResponse setNoChange(Boolean noChange) { + this.noChange = noChange; + return this; + } } /** diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 8b416b67b..7c02b3c2f 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableMap; import com.google.firebase.ErrorCode; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -149,6 +150,35 @@ public void testGetTemplate() throws FirebaseRemoteConfigException { assertEquals(TEST_ETAG, template.getETag()); } + @Test + public void testGetTemplateWithManagedValues() throws FirebaseRemoteConfigException { + Template template = new Template() + .setETag(TEST_ETAG) + .setParameters(ImmutableMap.of( + "p1", new Parameter().setConditionalValues(ImmutableMap.of( + "c1", ParameterValue.ofRollout("rollout_1", "value_1", 10.0))), + "p2", new Parameter().setConditionalValues(ImmutableMap.of( + "c2", ParameterValue.ofPersonalization("personalization_1"))) + )); + MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(template); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + Template fetchedTemplate = remoteConfig.getTemplate(); + + assertEquals(TEST_ETAG, fetchedTemplate.getETag()); + ParameterValue.RolloutValue rolloutValue = + (ParameterValue.RolloutValue) fetchedTemplate.getParameters().get("p1") + .getConditionalValues().get("c1"); + assertEquals("rollout_1", rolloutValue.getRolloutId()); + assertEquals("value_1", rolloutValue.getValue()); + assertEquals(10.0, rolloutValue.getPercent(), 0.0); + + ParameterValue.PersonalizationValue personalizationValue = + (ParameterValue.PersonalizationValue) fetchedTemplate.getParameters().get("p2") + .getConditionalValues().get("c2"); + assertEquals("personalization_1", personalizationValue.getPersonalizationId()); + } + @Test public void testGetTemplateFailure() { MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java index 7fc6ee555..598b0008c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterTest.java @@ -104,4 +104,22 @@ public void testEquality() { assertNotEquals(parameterThree, parameterFive); assertNotEquals(parameterThree, parameterSeven); } + + @Test + public void testEqualityWithManagedValues() { + final Parameter parameterOne = new Parameter() + .setDefaultValue(ParameterValue.ofRollout("rollout_1", "value_1", 10.0)); + final Parameter parameterTwo = new Parameter() + .setDefaultValue(ParameterValue.ofRollout("rollout_1", "value_1", 10.0)); + + assertEquals(parameterOne, parameterTwo); + + final Parameter parameterThree = new Parameter() + .setDefaultValue(ParameterValue.ofPersonalization("personalization_1")); + final Parameter parameterFour = new Parameter() + .setDefaultValue(ParameterValue.ofPersonalization("personalization_1")); + + assertEquals(parameterThree, parameterFour); + assertNotEquals(parameterOne, parameterThree); + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java index 842fd808f..91f908bd3 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java @@ -18,7 +18,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import com.google.common.collect.ImmutableList; +import com.google.firebase.remoteconfig.ParameterValue.ExperimentVariantValue; import org.junit.Test; public class ParameterValueTest { @@ -37,6 +40,45 @@ public void testCreateInAppDefault() { assertEquals(ParameterValue.InAppDefault.class, parameterValue.getClass()); } + @Test + public void testCreateRolloutValue() { + final ParameterValue.RolloutValue parameterValue = + ParameterValue.ofRollout("rollout_1", "value_1", 10.0); + + assertEquals("rollout_1", parameterValue.getRolloutId()); + assertEquals("value_1", parameterValue.getValue()); + assertEquals(10.0, parameterValue.getPercent(), 0.0); + } + + @Test + public void testCreatePersonalizationValue() { + final ParameterValue.PersonalizationValue parameterValue = + ParameterValue.ofPersonalization("personalization_1"); + + assertEquals("personalization_1", parameterValue.getPersonalizationId()); + } + + @Test + public void testCreateExperimentValue() { + final ParameterValue.ExperimentValue parameterValue = + ParameterValue.ofExperiment("experiment_1", ImmutableList.of( + ExperimentVariantValue.of("variant_1", "value_1"), + ExperimentVariantValue.ofNoChange("variant_2") + )); + + assertEquals("experiment_1", parameterValue.getExperimentId()); + assertEquals(2, parameterValue.getExperimentVariantValues().size()); + ExperimentVariantValue variant1 = parameterValue.getExperimentVariantValues().get(0); + assertEquals("variant_1", variant1.getVariantId()); + assertEquals("value_1", variant1.getValue()); + assertEquals(false, variant1.isNoChange()); + assertEquals(null, variant1.getNoChange()); + ExperimentVariantValue variant2 = parameterValue.getExperimentVariantValues().get(1); + assertEquals("variant_2", variant2.getVariantId()); + assertEquals(null, variant2.getValue()); + assertEquals(true, variant2.isNoChange()); + } + @Test public void testEquality() { ParameterValue.Explicit parameterValueOne = ParameterValue.of("value"); @@ -50,5 +92,46 @@ public void testEquality() { ParameterValue.InAppDefault parameterValueFive = ParameterValue.inAppDefault(); assertEquals(parameterValueFour, parameterValueFive); + + ParameterValue.RolloutValue rolloutValueOne = + ParameterValue.ofRollout("rollout_1", "value_1", 10.0); + ParameterValue.RolloutValue rolloutValueTwo = + ParameterValue.ofRollout("rollout_1", "value_1", 10.0); + ParameterValue.RolloutValue rolloutValueThree = + ParameterValue.ofRollout("rollout_2", "value_1", 10.0); + + assertEquals(rolloutValueOne, rolloutValueTwo); + assertNotEquals(rolloutValueOne, rolloutValueThree); + + ParameterValue.PersonalizationValue personalizationValueOne = + ParameterValue.ofPersonalization("personalization_1"); + ParameterValue.PersonalizationValue personalizationValueTwo = + ParameterValue.ofPersonalization("personalization_1"); + ParameterValue.PersonalizationValue personalizationValueThree = + ParameterValue.ofPersonalization("personalization_2"); + + assertEquals(personalizationValueOne, personalizationValueTwo); + assertNotEquals(personalizationValueOne, personalizationValueThree); + + ParameterValue.ExperimentValue experimentValueOne = + ParameterValue.ofExperiment("experiment_1", ImmutableList.of( + ExperimentVariantValue.of("variant_1", "value_1") + )); + ParameterValue.ExperimentValue experimentValueTwo = + ParameterValue.ofExperiment("experiment_1", ImmutableList.of( + ExperimentVariantValue.of("variant_1", "value_1") + )); + ParameterValue.ExperimentValue experimentValueThree = + ParameterValue.ofExperiment("experiment_2", ImmutableList.of( + ExperimentVariantValue.of("variant_1", "value_1") + )); + ParameterValue.ExperimentValue experimentValueFour = + ParameterValue.ofExperiment("experiment_1", ImmutableList.of( + ExperimentVariantValue.of("variant_2", "value_2") + )); + + assertEquals(experimentValueOne, experimentValueTwo); + assertNotEquals(experimentValueOne, experimentValueThree); + assertNotEquals(experimentValueOne, experimentValueFour); } } From 472d0aca64d88b7c0abf4b45ebfc08022538f270 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:58:18 -0500 Subject: [PATCH 02/11] chore(deps): Bump com.google.cloud:libraries-bom from 26.74.0 to 26.75.0 (#1179) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.74.0 to 26.75.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.74.0...v26.75.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.75.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b6098b851..29caec87c 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.74.0 + 26.75.0 pom import From 72d4b7da451700e80d9608ea0b2ac1d0a5bfc0b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:00:45 -0500 Subject: [PATCH 03/11] chore(deps-dev): Bump org.apache.maven.plugins:maven-compiler-plugin (#1180) Bumps [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.14.1 to 3.15.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.14.1...maven-compiler-plugin-3.15.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.15.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 29caec87c..06f42875d 100644 --- a/pom.xml +++ b/pom.xml @@ -263,7 +263,7 @@ maven-compiler-plugin - 3.14.1 + 3.15.0 1.8 1.8 From b095a066e97c53e38c328515312e75701dbfe3e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:30:56 -0500 Subject: [PATCH 04/11] chore(deps): Bump netty.version from 4.2.9.Final to 4.2.10.Final (#1178) Bumps `netty.version` from 4.2.9.Final to 4.2.10.Final. Updates `io.netty:netty-codec-http` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-handler` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) Updates `io.netty:netty-transport` from 4.2.9.Final to 4.2.10.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.9.Final...netty-4.2.10.Final) --- updated-dependencies: - dependency-name: io.netty:netty-codec-http dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-handler dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.netty:netty-transport dependency-version: 4.2.10.Final dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 06f42875d..e51fbdabd 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.2.9.Final + 4.2.10.Final From 2d66ae93ef62e8e6c3d3a7a24dbb7684319ad747 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:27:07 -0500 Subject: [PATCH 05/11] fix: Reimplement HTTP/2 response entity consumption using ApacheHttp2AsyncEntityConsumer and ApacheHttp2Entity (#1181) * fix: Reimplement HTTP/2 response entity consumption using ApacheHttp2AsyncEntityConsumer and ApacheHttp2Entity * fix: Address gemini review --- .../ApacheHttp2AsyncEntityConsumer.java | 85 ++++++++++++++ .../firebase/internal/ApacheHttp2Entity.java | 37 ++++++ .../firebase/internal/ApacheHttp2Request.java | 28 +++-- .../internal/ApacheHttp2Response.java | 45 +++++--- .../internal/ApacheHttp2TransportTest.java | 105 +++++++++++++++++- 5 files changed, 269 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java create mode 100644 src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java new file mode 100644 index 000000000..ee8733586 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2AsyncEntityConsumer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2026 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.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.nio.AsyncEntityConsumer; +import org.apache.hc.core5.http.nio.CapacityChannel; + +public class ApacheHttp2AsyncEntityConsumer implements AsyncEntityConsumer { + + private EntityDetails entityDetails; + private FutureCallback resultCallback; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + @Override + public void streamStart( + final EntityDetails entityDetails, + final FutureCallback resultCallback) + throws HttpException, IOException { + this.entityDetails = entityDetails; + this.resultCallback = resultCallback; + } + + @Override + public void updateCapacity(CapacityChannel capacityChannel) throws IOException { + capacityChannel.update(Integer.MAX_VALUE); + } + + @Override + public void consume(ByteBuffer src) throws IOException { + if (src.hasArray()) { + buffer.write(src.array(), src.arrayOffset() + src.position(), src.remaining()); + src.position(src.limit()); + } else { + byte[] bytes = new byte[src.remaining()]; + src.get(bytes); + buffer.write(bytes); + } + } + + @Override + public void streamEnd(List trailers) throws HttpException, IOException { + if (resultCallback != null) { + resultCallback.completed(getContent()); + } + } + + @Override + public void failed(Exception cause) { + if (resultCallback != null) { + resultCallback.failed(cause); + } + } + + @Override + public void releaseResources() { + buffer.reset(); + } + + @Override + public ApacheHttp2Entity getContent() { + return new ApacheHttp2Entity(buffer.toByteArray(), entityDetails); + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java new file mode 100644 index 000000000..9769a28a8 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Entity.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 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.internal; + +import org.apache.hc.core5.http.EntityDetails; + +public class ApacheHttp2Entity { + private final byte[] content; + private final EntityDetails entityDetails; + + public ApacheHttp2Entity(byte[] content, EntityDetails entityDetails) { + this.content = content; + this.entityDetails = entityDetails; + } + + public byte[] getContent() { + return content; + } + + public EntityDetails getEntityDetails() { + return entityDetails; + } +} diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java index a711dc0ac..ceb213c52 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Request.java @@ -32,13 +32,14 @@ import org.apache.hc.client5.http.ConnectTimeoutException; import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; -import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; -import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; import org.apache.hc.core5.http2.H2StreamResetException; import org.apache.hc.core5.util.Timeout; @@ -49,6 +50,7 @@ final class ApacheHttp2Request extends LowLevelHttpRequest { private final RequestConfig.Builder requestConfig; private int writeTimeout; private ApacheHttp2AsyncEntityProducer entityProducer; + private ApacheHttp2AsyncEntityConsumer entityConsumer; ApacheHttp2Request( CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { @@ -85,17 +87,20 @@ public LowLevelHttpResponse execute() throws IOException { // Build request request = requestBuilder.build(); - // Make Producer + // Make Entity Producer CompletableFuture writeFuture = new CompletableFuture<>(); entityProducer = new ApacheHttp2AsyncEntityProducer(this, writeFuture); + // Make Entity Consumer + entityConsumer = new ApacheHttp2AsyncEntityConsumer(); + // Execute - final Future responseFuture = httpAsyncClient.execute( + final Future> responseFuture = httpAsyncClient.execute( new BasicRequestProducer(request, entityProducer), - SimpleResponseConsumer.create(), - new FutureCallback() { + new BasicResponseConsumer(entityConsumer), + new FutureCallback>() { @Override - public void completed(final SimpleHttpResponse response) { + public void completed(final Message response) { } @Override @@ -120,10 +125,10 @@ public void cancelled() { // Wait for response try { - final SimpleHttpResponse response = responseFuture.get(); + final Message response = responseFuture.get(); return new ApacheHttp2Response(response); } catch (ExecutionException e) { - if (e.getCause() instanceof ConnectTimeoutException + if (e.getCause() instanceof ConnectTimeoutException || e.getCause() instanceof SocketTimeoutException) { throw new IOException("Connection Timeout", e.getCause()); } else if (e.getCause() instanceof HttpHostConnectException) { @@ -144,4 +149,9 @@ public void cancelled() { ApacheHttp2AsyncEntityProducer getEntityProducer() { return entityProducer; } + + @VisibleForTesting + ApacheHttp2AsyncEntityConsumer getEntityConsumer() { + return entityConsumer; + } } diff --git a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java index 329ec3970..7d277b972 100644 --- a/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java +++ b/src/main/java/com/google/firebase/internal/ApacheHttp2Response.java @@ -23,17 +23,27 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; -import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.message.StatusLine; public class ApacheHttp2Response extends LowLevelHttpResponse { - private final SimpleHttpResponse response; + private final Message message; + private final HttpResponse response; private final Header[] allHeaders; + private final EntityDetails entity; + private final byte[] content; - ApacheHttp2Response(SimpleHttpResponse response) { - this.response = response; - allHeaders = response.getHeaders(); + ApacheHttp2Response(Message message) { + this.message = message; + this.response = message.getHead(); + this.allHeaders = response.getHeaders(); + + ApacheHttp2Entity body = message.getBody(); + this.entity = body != null ? body.getEntityDetails() : null; + this.content = body != null ? body.getContent() : null; } @Override @@ -43,25 +53,25 @@ public int getStatusCode() { @Override public InputStream getContent() throws IOException { - return new ByteArrayInputStream(response.getBodyBytes()); + return content == null ? null : new ByteArrayInputStream(content); } @Override public String getContentEncoding() { - Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); - return contentEncodingHeader == null ? null : contentEncodingHeader.getValue(); + return entity == null ? null : entity.getContentEncoding(); } @Override public long getContentLength() { - String bodyText = response.getBodyText(); - return bodyText == null ? 0 : bodyText.length(); + if (content != null) { + return content.length; + } + return entity == null ? -1 : entity.getContentLength(); } @Override public String getContentType() { - ContentType contentType = response.getContentType(); - return contentType == null ? null : contentType.toString(); + return entity == null ? null : entity.getContentType(); } @Override @@ -71,11 +81,12 @@ public String getReasonPhrase() { @Override public String getStatusLine() { - return response.toString(); + return new StatusLine(response).toString(); } public String getHeaderValue(String name) { - return response.getLastHeader(name).getValue(); + Header header = response.getLastHeader(name); + return header == null ? null : header.getValue(); } @Override @@ -94,7 +105,7 @@ public String getHeaderName(int index) { } @VisibleForTesting - public SimpleHttpResponse getResponse() { - return response; + public Message getMessage() { + return message; } } diff --git a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java index 60b053109..ba604d3d1 100644 --- a/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java +++ b/src/test/java/com/google/firebase/internal/ApacheHttp2TransportTest.java @@ -56,11 +56,13 @@ import org.apache.hc.core5.http.HttpRequestMapper; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.HttpServer; import org.apache.hc.core5.http.impl.io.HttpService; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.support.BasicHttpServerRequestHandler; +import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.nio.AsyncPushConsumer; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; @@ -87,7 +89,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture( + new Message(new BasicHttpResponse(200), null)); } }, requestBuilder); @@ -118,7 +122,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture( + new Message(new BasicHttpResponse(200), null)); } }, requestBuilder); @@ -148,7 +154,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(simpleHttpResponse); + return (Future) CompletableFuture + .completedFuture(new Message(simpleHttpResponse, + new ApacheHttp2Entity(simpleHttpResponse.getBodyBytes(), null))); } }, requestBuilder); LowLevelHttpResponse response = request.execute(); @@ -157,7 +165,7 @@ public Future doExecute( // we confirm that the simple response we prepared in this test is the same as // the content's response assertTrue(response.getContent() instanceof ByteArrayInputStream); - assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getResponse()); + assertEquals(simpleHttpResponse, ((ApacheHttp2Response) response).getMessage().getHead()); // No need to cloase ByteArrayInputStream since close() has no effect. } @@ -212,7 +220,9 @@ public Future doExecute( final HandlerFactory pushHandlerFactory, final HttpContext context, final FutureCallback callback) { - return (Future) CompletableFuture.completedFuture(new SimpleHttpResponse(200)); + return (Future) CompletableFuture + .completedFuture(new Message( + new BasicHttpResponse(200), null)); } }; ApacheHttp2Transport transport = new ApacheHttp2Transport(mockClient); @@ -328,6 +338,91 @@ private void execute(ApacheHttp2Request request) throws IOException { request.execute(); } + @Test + public void testGzipResponse() throws IOException { + final String originalContent = "hello world"; + final java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + try (java.util.zip.GZIPOutputStream gzip = new java.util.zip.GZIPOutputStream(baos)) { + gzip.write(originalContent.getBytes(StandardCharsets.UTF_8)); + } + final byte[] gzippedContent = baos.toByteArray(); + + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + response.setCode(HttpStatus.SC_OK); + response.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(gzippedContent.length)); + response.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8"); + ByteArrayEntity entity = new ByteArrayEntity(gzippedContent, ContentType.TEXT_PLAIN); + response.setEntity(entity); + } + }; + + try (FakeServer server = new FakeServer(handler)) { + ApacheHttp2Transport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo"); + testUrl.setPort(server.getPort()); + + // Execute the low-level request directly to accurately assert metadata + // without Google's parsed HttpHeaders map mangling the payload lengths. + ApacheHttp2Request request = transport.buildRequest("GET", testUrl.build()); + LowLevelHttpResponse response = request.execute(); + + assertEquals(200, response.getStatusCode()); + assertEquals("text/plain; charset=UTF-8", response.getContentType()); + + boolean wasAutoDecompressed = response.getContentEncoding() == null; + if (wasAutoDecompressed) { + assertEquals(originalContent.length(), response.getContentLength()); + } else { + assertEquals("gzip", response.getContentEncoding()); + assertEquals(gzippedContent.length, response.getContentLength()); + } + + // Verify the low-level stream returns the exact expected payload based on + // decompression state + java.io.InputStream stream = response.getContent(); + byte[] resultBytes = com.google.common.io.ByteStreams.toByteArray(stream); + + if (wasAutoDecompressed) { + assertEquals(originalContent, new String(resultBytes, StandardCharsets.UTF_8)); + } else { + org.junit.Assert.assertArrayEquals(gzippedContent, resultBytes); + } + } + } + + @Test + public void testEmptyResponseWithHeaders() throws IOException { + // Tests that a response with no actual body but headers does not throw NPE + // in ApacheHttp2Response due to entity being null. + final HttpRequestHandler handler = new HttpRequestHandler() { + @Override + public void handle( + ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) + throws HttpException, IOException { + response.setCode(HttpStatus.SC_NO_CONTENT); + response.setHeader(HttpHeaders.CONTENT_LENGTH, "0"); + // Explicitly omitting the entity to simulate NO_CONTENT bodyless response + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttp2Transport(); + GenericUrl testUrl = new GenericUrl("http://localhost/empty"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpResponse response = transport.createRequestFactory() + .buildGetRequest(testUrl) + .execute(); + + assertEquals(204, response.getStatusCode()); + assertEquals(0L, response.getHeaders().getContentLength().longValue()); + } + } + private static class FakeServer implements AutoCloseable { private final HttpServer server; From b29ac825c7d78b7aa17c01ee0775352cdbd6c849 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:45:39 -0500 Subject: [PATCH 06/11] chore(deps): Bump com.google.cloud:libraries-bom from 26.75.0 to 26.76.0 (#1185) Bumps [com.google.cloud:libraries-bom](https://github.com/googleapis/java-cloud-bom) from 26.75.0 to 26.76.0. - [Release notes](https://github.com/googleapis/java-cloud-bom/releases) - [Changelog](https://github.com/googleapis/java-cloud-bom/blob/main/release-please-config.json) - [Commits](https://github.com/googleapis/java-cloud-bom/compare/v26.75.0...v26.76.0) --- updated-dependencies: - dependency-name: com.google.cloud:libraries-bom dependency-version: 26.76.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e51fbdabd..8c0093c0f 100644 --- a/pom.xml +++ b/pom.xml @@ -378,7 +378,7 @@ com.google.cloud libraries-bom - 26.75.0 + 26.76.0 pom import From a568b65964a596a78e1b3d468c9e7243778d6fe7 Mon Sep 17 00:00:00 2001 From: Ashish Kothari Date: Wed, 25 Feb 2026 21:57:46 +0530 Subject: [PATCH 07/11] Add javadocs to public methods (#1187) --- .../firebase/remoteconfig/ParameterValue.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java index 7bf9d3ba0..db63e3718 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java +++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java @@ -205,14 +205,30 @@ private RolloutValue(String rolloutId, String value, double percent) { this.percent = percent; } + /** + * Gets the ID of the Rollout linked to this value. + * + * @return The Rollout ID + */ public String getRolloutId() { return rolloutId; } + /** + * Gets the value that is being rolled out. + * + * @return The rollout value + */ public String getValue() { return value; } + /** + * Gets the rollout percentage representing the exposure of rollout value + * in the target audience. + * + * @return Percentage of audience exposed to the rollout + */ public double getPercent() { return percent; } @@ -256,6 +272,11 @@ private PersonalizationValue(String personalizationId) { this.personalizationId = personalizationId; } + /** + * Gets the ID of the Personalization linked to this value. + * + * @return The Personalization ID + */ public String getPersonalizationId() { return personalizationId; } @@ -320,10 +341,20 @@ public static ExperimentVariantValue ofNoChange(String variantId) { return new ExperimentVariantValue(variantId, null, true); } + /** + * Gets the ID of the experiment variant. + * + * @return The variant ID + */ public String getVariantId() { return variantId; } + /** + * Gets the value of the experiment variant. + * + * @return The variant value + */ @Nullable public String getValue() { return value; @@ -334,6 +365,11 @@ Boolean getNoChange() { return noChange; } + /** + * Returns whether the experiment variant is a no-change variant. + * + * @return true if the experiment variant is a no-change variant, and false otherwise. + */ public boolean isNoChange() { return Boolean.TRUE.equals(noChange); } @@ -370,10 +406,20 @@ private ExperimentValue(String experimentId, List varian this.variantValues = variantValues; } + /** + * Gets the ID of the experiment linked to this value. + * + * @return The Experiment ID + */ public String getExperimentId() { return experimentId; } + /** + * Gets a collection of variant values served by the experiment. + * + * @return List of {@link ExperimentVariantValue} + */ public List getExperimentVariantValues() { return variantValues; } From 5169aae2642a27ca8d423aa28365ca111d34a4d2 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:39:22 -0500 Subject: [PATCH 08/11] [chore] Release 9.8.0 (#1184) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8c0093c0f..a642c1936 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 9.7.1 + 9.8.0 jar firebase-admin From d69dcbd9604ca2eb49c32d27067df4b70a126c87 Mon Sep 17 00:00:00 2001 From: andrew-signal Date: Tue, 3 Mar 2026 12:06:07 -0500 Subject: [PATCH 09/11] Add support for new bandwidthConstrainedOk flag used to deliver notifications to sat networks (#1145) Co-authored-by: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> --- .../firebase/messaging/AndroidConfig.java | 14 +++++++++++ .../firebase/messaging/MessageTest.java | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index ac9d8a52f..941793479 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -55,6 +55,9 @@ public class AndroidConfig { @Key("direct_boot_ok") private final Boolean directBootOk; + @Key("bandwidth_constrained_ok") + private final Boolean bandwidthConstrainedOk; + private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { @@ -79,6 +82,7 @@ private AndroidConfig(Builder builder) { this.notification = builder.notification; this.fcmOptions = builder.fcmOptions; this.directBootOk = builder.directBootOk; + this.bandwidthConstrainedOk = builder.bandwidthConstrainedOk; } /** @@ -108,6 +112,7 @@ public static class Builder { private AndroidNotification notification; private AndroidFcmOptions fcmOptions; private Boolean directBootOk; + private Boolean bandwidthConstrainedOk; private Builder() {} @@ -218,6 +223,15 @@ public Builder setDirectBootOk(boolean directBootOk) { return this; } + /** + * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages can be delivered + * even when the device is connected through a bandwidth-constrained network. + */ + public Builder setBandwidthConstrainedOk(boolean bandwidthConstrainedOk) { + this.bandwidthConstrainedOk = bandwidthConstrainedOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 263d1ced5..70d130032 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -224,6 +224,29 @@ public void testAndroidMessageWithDirectBootOk() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithBandwidthConstrainedOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setBandwidthConstrainedOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "bandwidth_constrained_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); From d04e15b73b4972c1e6e50ed14ca1c2956e761266 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <145066863+jonathanedey@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:48:34 -0400 Subject: [PATCH 10/11] feat(fcm): Add support for restricted satellite API (#1191) --- .../firebase/messaging/AndroidConfig.java | 18 +++++++++++-- .../firebase/messaging/MessageTest.java | 26 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java index 941793479..22f591680 100644 --- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java +++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java @@ -58,6 +58,9 @@ public class AndroidConfig { @Key("bandwidth_constrained_ok") private final Boolean bandwidthConstrainedOk; + @Key("restricted_satellite_ok") + private final Boolean restrictedSatelliteOk; + private AndroidConfig(Builder builder) { this.collapseKey = builder.collapseKey; if (builder.priority != null) { @@ -83,6 +86,7 @@ private AndroidConfig(Builder builder) { this.fcmOptions = builder.fcmOptions; this.directBootOk = builder.directBootOk; this.bandwidthConstrainedOk = builder.bandwidthConstrainedOk; + this.restrictedSatelliteOk = builder.restrictedSatelliteOk; } /** @@ -113,6 +117,7 @@ public static class Builder { private AndroidFcmOptions fcmOptions; private Boolean directBootOk; private Boolean bandwidthConstrainedOk; + private Boolean restrictedSatelliteOk; private Builder() {} @@ -224,14 +229,23 @@ public Builder setDirectBootOk(boolean directBootOk) { } /** - * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages can be delivered - * even when the device is connected through a bandwidth-constrained network. + * Sets the {@code bandwidth_constrained_ok} flag. If set to true, messages will be allowed + * to be delivered to the app while the device is on a bandwidth constrained network. */ public Builder setBandwidthConstrainedOk(boolean bandwidthConstrainedOk) { this.bandwidthConstrainedOk = bandwidthConstrainedOk; return this; } + /** + * Sets the {@code restricted_satellite_ok} flag. If set to true, messages will be allowed + * to be delivered to the app while the device is on a restricted satellite network. + */ + public Builder setRestrictedSatelliteOk(boolean restrictedSatelliteOk) { + this.restrictedSatelliteOk = restrictedSatelliteOk; + return this; + } + /** * Creates a new {@link AndroidConfig} instance from the parameters set on this builder. * diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java index 70d130032..94ca4fa4a 100644 --- a/src/test/java/com/google/firebase/messaging/MessageTest.java +++ b/src/test/java/com/google/firebase/messaging/MessageTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; -import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonParser; import com.google.common.collect.ImmutableList; @@ -247,6 +246,29 @@ public void testAndroidMessageWithBandwidthConstrainedOk() throws IOException { assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); } + @Test + public void testAndroidMessageWithRestrictedSatelliteOk() throws IOException { + Message message = Message.builder() + .setAndroidConfig(AndroidConfig.builder() + .setRestrictedSatelliteOk(true) + .setNotification(AndroidNotification.builder() + .setTitle("android-title") + .setBody("android-body") + .build()) + .build()) + .setTopic("test-topic") + .build(); + Map notification = ImmutableMap.builder() + .put("title", "android-title") + .put("body", "android-body") + .build(); + Map data = ImmutableMap.of( + "restricted_satellite_ok", true, + "notification", notification + ); + assertJsonEquals(ImmutableMap.of("topic", "test-topic", "android", data), message); + } + @Test(expected = IllegalArgumentException.class) public void testAndroidNotificationWithNegativeCount() throws IllegalArgumentException { AndroidNotification.builder().setNotificationCount(-1).build(); @@ -991,7 +1013,7 @@ public void testExtendedAndroidNotificationParameters() throws IOException { } private static void assertJsonEquals( - Map expected, Object actual) throws IOException { + Map expected, Object actual) throws IOException { assertEquals(expected, toMap(actual)); } From ab858ef2004c69c1ffee5a7dbb6144618646e206 Mon Sep 17 00:00:00 2001 From: varun rathore <35365856+rathovarun1032@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:28:26 +0530 Subject: [PATCH 11/11] feat(rc):Add exposurePercent to ExperimentValues (#1201) * Add exposurePercent to ExperimentValues * fix comments * fix build * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix build * fix build * fix build * fix build * fix build * fix build * fix build * resolved comments * resolved comments * resolved comments * resolved comments * Update TemplateResponse.java * Update Template.java --------- Co-authored-by: Varun Rathore Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../firebase/remoteconfig/ParameterValue.java | 133 ++++++++++-------- .../firebase/remoteconfig/Template.java | 58 +++++++- .../internal/TemplateResponse.java | 12 ++ .../remoteconfig/ParameterValueTest.java | 34 ++++- .../firebase/remoteconfig/TemplateTest.java | 2 +- 5 files changed, 172 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java index db63e3718..1d48bbb03 100644 --- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java +++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java @@ -26,14 +26,12 @@ import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.PersonalizationValueResponse; import com.google.firebase.remoteconfig.internal.TemplateResponse.RolloutValueResponse; - -import java.util.ArrayList; import java.util.List; import java.util.Objects; -/** - * Represents a Remote Config parameter value that can be used in a {@link Template}. - */ +/** Represents a Remote Config parameter value + * that can be used in a + * {@link Template}. */ public abstract class ParameterValue { /** @@ -82,17 +80,18 @@ public static PersonalizationValue ofPersonalization(String personalizationId) { * * @param experimentId The experiment ID. * @param variantValues The list of experiment variant values. + * @param exposurePercent The exposure percentage of the experiment. * @return A {@link ParameterValue.ExperimentValue} instance. */ - public static ExperimentValue ofExperiment(String experimentId, - List variantValues) { - return new ExperimentValue(experimentId, variantValues); + public static ExperimentValue ofExperiment( + String experimentId, List variantValues, double exposurePercent) { + return new ExperimentValue(experimentId, variantValues, exposurePercent); } abstract ParameterValueResponse toParameterValueResponse(); static ParameterValue fromParameterValueResponse( - @NonNull ParameterValueResponse parameterValueResponse) { + @NonNull ParameterValueResponse parameterValueResponse) { checkNotNull(parameterValueResponse); if (parameterValueResponse.isUseInAppDefault()) { return ParameterValue.inAppDefault(); @@ -102,7 +101,7 @@ static ParameterValue fromParameterValueResponse( // Protobuf serialization does not set values for fields on the wire when // they are equal to the default value for the field type. When deserializing, // can appear as the value not being set. Explicitly handle default value for - // the percent field since 0 is a valid value. + // the percent field since 0 is a valid value. double percent = 0; if (rv.getPercent() != null) { percent = rv.getPercent(); @@ -115,18 +114,26 @@ static ParameterValue fromParameterValueResponse( } if (parameterValueResponse.getExperimentValue() != null) { ExperimentValueResponse ev = parameterValueResponse.getExperimentValue(); - List variantValues = ev.getExperimentVariantValues().stream() - .map(evv -> new ExperimentVariantValue( - evv.getVariantId(), evv.getValue(), evv.getNoChange())) - .collect(toList()); - return ParameterValue.ofExperiment(ev.getExperimentId(), variantValues); + List variantValues = + ev.getExperimentVariantValues().stream() + .map( + evv -> + new ExperimentVariantValue( + evv.getVariantId(), evv.getValue(), evv.getNoChange())) + .collect(toList()); + // Handle null exposurePercent by defaulting to 0 + double exposurePercent = 0; + if (ev.getExposurePercent() != null) { + exposurePercent = ev.getExposurePercent(); + } + return ParameterValue.ofExperiment( + ev.getExperimentId(), variantValues, exposurePercent); } return ParameterValue.of(parameterValueResponse.getValue()); } /** - * Represents an explicit Remote Config parameter value with a value that the - * parameter is set to. + * Represents an explicit Remote Config parameter value with a value that the parameter is set to. */ public static final class Explicit extends ParameterValue { @@ -147,8 +154,7 @@ public String getValue() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse() - .setValue(this.value); + return new ParameterValueResponse().setValue(this.value); } @Override @@ -169,9 +175,7 @@ public int hashCode() { } } - /** - * Represents an in app default parameter value. - */ + /** Represents an in app default parameter value. */ public static final class InAppDefault extends ParameterValue { @Override @@ -191,9 +195,7 @@ public boolean equals(Object o) { } } - /** - * Represents a Rollout value. - */ + /** Represents a Rollout value. */ public static final class RolloutValue extends ParameterValue { private final String rolloutId; private final String value; @@ -224,8 +226,8 @@ public String getValue() { } /** - * Gets the rollout percentage representing the exposure of rollout value - * in the target audience. + * Gets the rollout percentage representing the exposure of rollout value in the target + * audience. * * @return Percentage of audience exposed to the rollout */ @@ -235,11 +237,12 @@ public double getPercent() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse().setRolloutValue( + return new ParameterValueResponse() + .setRolloutValue( new RolloutValueResponse() - .setRolloutId(this.rolloutId) - .setValue(this.value) - .setPercent(this.percent)); + .setRolloutId(this.rolloutId) + .setValue(this.value) + .setPercent(this.percent)); } @Override @@ -252,8 +255,8 @@ public boolean equals(Object o) { } RolloutValue that = (RolloutValue) o; return Double.compare(that.percent, percent) == 0 - && Objects.equals(rolloutId, that.rolloutId) - && Objects.equals(value, that.value); + && Objects.equals(rolloutId, that.rolloutId) + && Objects.equals(value, that.value); } @Override @@ -262,9 +265,7 @@ public int hashCode() { } } - /** - * Represents a Personalization value. - */ + /** Represents a Personalization value. */ public static final class PersonalizationValue extends ParameterValue { private final String personalizationId; @@ -283,9 +284,9 @@ public String getPersonalizationId() { @Override ParameterValueResponse toParameterValueResponse() { - return new ParameterValueResponse().setPersonalizationValue( - new PersonalizationValueResponse() - .setPersonalizationId(this.personalizationId)); + return new ParameterValueResponse() + .setPersonalizationValue( + new PersonalizationValueResponse().setPersonalizationId(this.personalizationId)); } @Override @@ -306,9 +307,7 @@ public int hashCode() { } } - /** - * Represents a specific variant within an Experiment. - */ + /** Represents a specific variant within an Experiment. */ public static final class ExperimentVariantValue { private final String variantId; private final String value; @@ -384,8 +383,8 @@ public boolean equals(Object o) { } ExperimentVariantValue that = (ExperimentVariantValue) o; return noChange == that.noChange - && Objects.equals(variantId, that.variantId) - && Objects.equals(value, that.value); + && Objects.equals(variantId, that.variantId) + && Objects.equals(value, that.value); } @Override @@ -394,16 +393,17 @@ public int hashCode() { } } - /** - * Represents an Experiment value. - */ + /** Represents an Experiment value. */ public static final class ExperimentValue extends ParameterValue { private final String experimentId; private final List variantValues; + private final double exposurePercent; - private ExperimentValue(String experimentId, List variantValues) { + private ExperimentValue( + String experimentId, List variantValues, double exposurePercent) { this.experimentId = experimentId; this.variantValues = variantValues; + this.exposurePercent = exposurePercent; } /** @@ -415,6 +415,15 @@ public String getExperimentId() { return experimentId; } + /** + * Gets the exposure percentage of the experiment linked to this value. + * + * @return Exposure percentage of the experiment linked to this value. + */ + public double getExposurePercent() { + return exposurePercent; + } + /** * Gets a collection of variant values served by the experiment. * @@ -426,16 +435,21 @@ public List getExperimentVariantValues() { @Override ParameterValueResponse toParameterValueResponse() { - List variantValueResponses = variantValues.stream() - .map(variantValue -> new ExperimentVariantValueResponse() - .setVariantId(variantValue.getVariantId()) - .setValue(variantValue.getValue()) - .setNoChange(variantValue.getNoChange())) - .collect(toList()); - return new ParameterValueResponse().setExperimentValue( + List variantValueResponses = + variantValues.stream() + .map( + variantValue -> + new ExperimentVariantValueResponse() + .setVariantId(variantValue.getVariantId()) + .setValue(variantValue.getValue()) + .setNoChange(variantValue.getNoChange())) + .collect(toList()); + return new ParameterValueResponse() + .setExperimentValue( new ExperimentValueResponse() - .setExperimentId(this.experimentId) - .setExperimentVariantValues(variantValueResponses)); + .setExperimentId(this.experimentId) + .setExperimentVariantValues(variantValueResponses) + .setExposurePercent(this.exposurePercent)); } @Override @@ -448,12 +462,13 @@ public boolean equals(Object o) { } ExperimentValue that = (ExperimentValue) o; return Objects.equals(experimentId, that.experimentId) - && Objects.equals(variantValues, that.variantValues); + && Objects.equals(variantValues, that.variantValues) + && Double.compare(that.exposurePercent, exposurePercent) == 0; } @Override public int hashCode() { - return Objects.hash(experimentId, variantValues); + return Objects.hash(experimentId, variantValues, exposurePercent); } } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index d94cfc89e..5dfe06aaf 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -60,7 +60,7 @@ public Template(String etag) { this((String) null); } - Template(@NonNull TemplateResponse templateResponse) { + Template(@NonNull TemplateResponse templateResponse) throws FirebaseRemoteConfigException { checkNotNull(templateResponse); this.parameters = new HashMap<>(); this.conditions = new ArrayList<>(); @@ -86,6 +86,7 @@ public Template(String etag) { if (templateResponse.getVersion() != null) { this.version = new Version(templateResponse.getVersion()); } + validateExperimentExposurePercents(this.parameters, this.parameterGroups); this.etag = templateResponse.getEtag(); } @@ -278,4 +279,59 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(etag, parameters, conditions, parameterGroups, version); } + + private void validateExperimentExposurePercents( + Map parameters, + Map parameterGroups) throws FirebaseRemoteConfigException { + Map experimentExposurePercents = new HashMap<>(); + validateParameters(parameters, experimentExposurePercents); + if (parameterGroups != null) { + for (ParameterGroup group : parameterGroups.values()) { + validateParameters(group.getParameters(), experimentExposurePercents); + } + } + } + + private void validateParameters( + Map parameters, + Map experimentExposurePercents) throws FirebaseRemoteConfigException { + if (parameters == null) { + return; + } + for (Map.Entry entry : parameters.entrySet()) { + Parameter parameter = entry.getValue(); + String parameterName = entry.getKey(); + checkExposurePercent(parameter.getDefaultValue(), parameterName, experimentExposurePercents); + if (parameter.getConditionalValues() != null) { + for (ParameterValue value : parameter.getConditionalValues().values()) { + checkExposurePercent(value, parameterName, experimentExposurePercents); + } + } + } + } + + private void checkExposurePercent( + ParameterValue value, + String parameterName, + Map experimentExposurePercents) throws FirebaseRemoteConfigException { + if (value instanceof ParameterValue.ExperimentValue) { + ParameterValue.ExperimentValue experimentValue = (ParameterValue.ExperimentValue) value; + Double exposurePercent = experimentValue.getExposurePercent(); + if (exposurePercent != null) { + // Enforce range [0, 100] + if (exposurePercent < 0 || exposurePercent > 100) { + return; + } + // Enforce consistency for the same experimentId + String experimentId = experimentValue.getExperimentId(); + if (experimentExposurePercents.containsKey(experimentId)) { + if (!Objects.equals(experimentExposurePercents.get(experimentId), exposurePercent)) { + return; + } + } else { + experimentExposurePercents.put(experimentId, exposurePercent); + } + } + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index b92580f8c..32f5bcef9 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -288,6 +288,9 @@ public static final class ExperimentValueResponse { @Key("variantValue") private List experimentVariantValues; + @Key("exposurePercent") + private Double exposurePercent; + public String getExperimentId() { return experimentId; } @@ -296,6 +299,10 @@ public List getExperimentVariantValues() { return experimentVariantValues; } + public Double getExposurePercent() { + return exposurePercent; + } + public ExperimentValueResponse setExperimentId(String experimentId) { this.experimentId = experimentId; return this; @@ -306,6 +313,11 @@ public ExperimentValueResponse setExperimentVariantValues( this.experimentVariantValues = experimentVariantValues; return this; } + + public ExperimentValueResponse setExposurePercent(Double exposurePercent) { + this.exposurePercent = exposurePercent; + return this; + } } /** diff --git a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java index 91f908bd3..46db5c44e 100644 --- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.remoteconfig.ParameterValue.ExperimentVariantValue; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse; + import org.junit.Test; public class ParameterValueTest { @@ -64,7 +66,11 @@ public void testCreateExperimentValue() { ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1"), ExperimentVariantValue.ofNoChange("variant_2") - )); + ), 10.0); + + assertEquals("experiment_1", parameterValue.getExperimentId()); + assertEquals(2, parameterValue.getExperimentVariantValues().size()); + assertEquals(10.0, parameterValue.getExposurePercent(), 0.0); assertEquals("experiment_1", parameterValue.getExperimentId()); assertEquals(2, parameterValue.getExperimentVariantValues().size()); @@ -77,6 +83,7 @@ public void testCreateExperimentValue() { assertEquals("variant_2", variant2.getVariantId()); assertEquals(null, variant2.getValue()); assertEquals(true, variant2.isNoChange()); + } @Test @@ -116,22 +123,37 @@ public void testEquality() { ParameterValue.ExperimentValue experimentValueOne = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueTwo = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueThree = ParameterValue.ofExperiment("experiment_2", ImmutableList.of( ExperimentVariantValue.of("variant_1", "value_1") - )); + ), 10.0); ParameterValue.ExperimentValue experimentValueFour = ParameterValue.ofExperiment("experiment_1", ImmutableList.of( ExperimentVariantValue.of("variant_2", "value_2") - )); - + ), 20.0); assertEquals(experimentValueOne, experimentValueTwo); assertNotEquals(experimentValueOne, experimentValueThree); assertNotEquals(experimentValueOne, experimentValueFour); } + + @Test + public void testExperimentValueWithZeroExposure() { + ParameterValue.ExperimentValue value = ParameterValue.ofExperiment( + "exp_0", ImmutableList.of(ExperimentVariantValue.of("v1", "foo")), 0.0); + + // Test Serialization + ParameterValueResponse response = value.toParameterValueResponse(); + assertEquals(0.0, response.getExperimentValue().getExposurePercent(), 0.0); + + // Test Deserialization + ParameterValue fromResponse = ParameterValue.fromParameterValueResponse(response); + assertTrue(fromResponse instanceof ParameterValue.ExperimentValue); + assertEquals(0.0, ((ParameterValue.ExperimentValue) fromResponse).getExposurePercent(), 0.0); + } + } diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index a3ea3e878..92abb850c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -107,7 +107,7 @@ public void testConstructorWithETag() { } @Test(expected = NullPointerException.class) - public void testConstructorWithNullTemplateResponse() { + public void testConstructorWithNullTemplateResponse() throws FirebaseRemoteConfigException { new Template((TemplateResponse) null); }