diff --git a/pom.xml b/pom.xml
index b6098b851..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
@@ -59,7 +59,7 @@
UTF-8
UTF-8
${skipTests}
- 4.2.9.Final
+ 4.2.10.Final
@@ -263,7 +263,7 @@
maven-compiler-plugin
- 3.14.1
+ 3.15.0
1.8
1.8
@@ -378,7 +378,7 @@
com.google.cloud
libraries-bom
- 26.74.0
+ 26.76.0
pom
import
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 extends Header> 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/main/java/com/google/firebase/messaging/AndroidConfig.java b/src/main/java/com/google/firebase/messaging/AndroidConfig.java
index ac9d8a52f..22f591680 100644
--- a/src/main/java/com/google/firebase/messaging/AndroidConfig.java
+++ b/src/main/java/com/google/firebase/messaging/AndroidConfig.java
@@ -55,6 +55,12 @@ public class AndroidConfig {
@Key("direct_boot_ok")
private final Boolean directBootOk;
+ @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) {
@@ -79,6 +85,8 @@ private AndroidConfig(Builder builder) {
this.notification = builder.notification;
this.fcmOptions = builder.fcmOptions;
this.directBootOk = builder.directBootOk;
+ this.bandwidthConstrainedOk = builder.bandwidthConstrainedOk;
+ this.restrictedSatelliteOk = builder.restrictedSatelliteOk;
}
/**
@@ -108,6 +116,8 @@ public static class Builder {
private AndroidNotification notification;
private AndroidFcmOptions fcmOptions;
private Boolean directBootOk;
+ private Boolean bandwidthConstrainedOk;
+ private Boolean restrictedSatelliteOk;
private Builder() {}
@@ -218,6 +228,24 @@ public Builder setDirectBootOk(boolean directBootOk) {
return this;
}
+ /**
+ * 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/main/java/com/google/firebase/remoteconfig/ParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
index 90bf0e5df..1d48bbb03 100644
--- a/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
+++ b/src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
@@ -17,15 +17,21 @@
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.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 {
/**
@@ -47,20 +53,87 @@ 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.
+ * @param exposurePercent The exposure percentage of the experiment.
+ * @return A {@link ParameterValue.ExperimentValue} instance.
+ */
+ 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();
}
+ 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());
+ // 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 {
@@ -81,8 +154,7 @@ public String getValue() {
@Override
ParameterValueResponse toParameterValueResponse() {
- return new ParameterValueResponse()
- .setValue(this.value);
+ return new ParameterValueResponse().setValue(this.value);
}
@Override
@@ -103,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
@@ -124,4 +194,281 @@ 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ @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;
+ }
+
+ /**
+ * Gets the ID of the Personalization linked to this value.
+ *
+ * @return The Personalization ID
+ */
+ 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ @Nullable
+ 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);
+ }
+
+ @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 final double exposurePercent;
+
+ private ExperimentValue(
+ String experimentId, List variantValues, double exposurePercent) {
+ this.experimentId = experimentId;
+ this.variantValues = variantValues;
+ this.exposurePercent = exposurePercent;
+ }
+
+ /**
+ * Gets the ID of the experiment linked to this value.
+ *
+ * @return The Experiment ID
+ */
+ 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.
+ *
+ * @return List of {@link ExperimentVariantValue}
+ */
+ 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)
+ .setExposurePercent(this.exposurePercent));
+ }
+
+ @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)
+ && Double.compare(that.exposurePercent, exposurePercent) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ 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 57a066bf2..32f5bcef9 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,167 @@ 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;
+
+ @Key("exposurePercent")
+ private Double exposurePercent;
+
+ public String getExperimentId() {
+ return experimentId;
+ }
+
+ public List getExperimentVariantValues() {
+ return experimentVariantValues;
+ }
+
+ public Double getExposurePercent() {
+ return exposurePercent;
+ }
+
+ public ExperimentValueResponse setExperimentId(String experimentId) {
+ this.experimentId = experimentId;
+ return this;
+ }
+
+ public ExperimentValueResponse setExperimentVariantValues(
+ List experimentVariantValues) {
+ this.experimentVariantValues = experimentVariantValues;
+ return this;
+ }
+
+ public ExperimentValueResponse setExposurePercent(Double exposurePercent) {
+ this.exposurePercent = exposurePercent;
+ 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/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;
diff --git a/src/test/java/com/google/firebase/messaging/MessageTest.java b/src/test/java/com/google/firebase/messaging/MessageTest.java
index 263d1ced5..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;
@@ -224,6 +223,52 @@ 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
+ 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();
@@ -968,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));
}
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..46db5c44e 100644
--- a/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/ParameterValueTest.java
@@ -18,6 +18,11 @@
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 com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse;
import org.junit.Test;
@@ -37,6 +42,50 @@ 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")
+ ), 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());
+ 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 +99,61 @@ 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")
+ ), 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);
}