Skip to content
133 changes: 74 additions & 59 deletions src/main/java/com/google/firebase/remoteconfig/ParameterValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

/**
Expand Down Expand Up @@ -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<ExperimentVariantValue> variantValues) {
return new ExperimentValue(experimentId, variantValues);
public static ExperimentValue ofExperiment(
String experimentId, List<ExperimentVariantValue> 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();
Expand All @@ -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();
Expand All @@ -115,18 +114,26 @@ static ParameterValue fromParameterValueResponse(
}
if (parameterValueResponse.getExperimentValue() != null) {
ExperimentValueResponse ev = parameterValueResponse.getExperimentValue();
List<ExperimentVariantValue> variantValues = ev.getExperimentVariantValues().stream()
.map(evv -> new ExperimentVariantValue(
evv.getVariantId(), evv.getValue(), evv.getNoChange()))
.collect(toList());
return ParameterValue.ofExperiment(ev.getExperimentId(), variantValues);
List<ExperimentVariantValue> 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 {

Expand All @@ -147,8 +154,7 @@ public String getValue() {

@Override
ParameterValueResponse toParameterValueResponse() {
return new ParameterValueResponse()
.setValue(this.value);
return new ParameterValueResponse().setValue(this.value);
}

@Override
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<ExperimentVariantValue> variantValues;
private final double exposurePercent;

private ExperimentValue(String experimentId, List<ExperimentVariantValue> variantValues) {
private ExperimentValue(
String experimentId, List<ExperimentVariantValue> variantValues, double exposurePercent) {
this.experimentId = experimentId;
this.variantValues = variantValues;
this.exposurePercent = exposurePercent;
}

/**
Expand All @@ -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.
*
Expand All @@ -426,16 +435,21 @@ public List<ExperimentVariantValue> getExperimentVariantValues() {

@Override
ParameterValueResponse toParameterValueResponse() {
List<ExperimentVariantValueResponse> variantValueResponses = variantValues.stream()
.map(variantValue -> new ExperimentVariantValueResponse()
.setVariantId(variantValue.getVariantId())
.setValue(variantValue.getValue())
.setNoChange(variantValue.getNoChange()))
.collect(toList());
return new ParameterValueResponse().setExperimentValue(
List<ExperimentVariantValueResponse> 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
Expand All @@ -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);
}
}
}
58 changes: 57 additions & 1 deletion src/main/java/com/google/firebase/remoteconfig/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<>();
Expand All @@ -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();
}

Expand Down Expand Up @@ -278,4 +279,59 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(etag, parameters, conditions, parameterGroups, version);
}

private void validateExperimentExposurePercents(
Map<String, Parameter> parameters,
Map<String, ParameterGroup> parameterGroups) throws FirebaseRemoteConfigException {
Map<String, Double> experimentExposurePercents = new HashMap<>();
validateParameters(parameters, experimentExposurePercents);
if (parameterGroups != null) {
for (ParameterGroup group : parameterGroups.values()) {
validateParameters(group.getParameters(), experimentExposurePercents);
}
}
}

private void validateParameters(
Map<String, Parameter> parameters,
Map<String, Double> experimentExposurePercents) throws FirebaseRemoteConfigException {
if (parameters == null) {
return;
}
for (Map.Entry<String, Parameter> 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<String, Double> 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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ public static final class ExperimentValueResponse {
@Key("variantValue")
private List<ExperimentVariantValueResponse> experimentVariantValues;

@Key("exposurePercent")
private Double exposurePercent;

public String getExperimentId() {
return experimentId;
}
Expand All @@ -296,6 +299,10 @@ public List<ExperimentVariantValueResponse> getExperimentVariantValues() {
return experimentVariantValues;
}

public Double getExposurePercent() {
return exposurePercent;
}

public ExperimentValueResponse setExperimentId(String experimentId) {
this.experimentId = experimentId;
return this;
Expand All @@ -306,6 +313,11 @@ public ExperimentValueResponse setExperimentVariantValues(
this.experimentVariantValues = experimentVariantValues;
return this;
}

public ExperimentValueResponse setExposurePercent(Double exposurePercent) {
this.exposurePercent = exposurePercent;
return this;
}
}

/**
Expand Down
Loading
Loading