Skip to content

Commit

Permalink
Merge 5a15867 into 6a03d4c
Browse files Browse the repository at this point in the history
  • Loading branch information
tusharkhandelwal8 authored Nov 22, 2024
2 parents 6a03d4c + 5a15867 commit bda1af7
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 5 deletions.
12 changes: 12 additions & 0 deletions firebase-config/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ package com.google.firebase.remoteconfig {
method public void remove();
}

public class CustomSignals {
}

public static class CustomSignals.Builder {
ctor public CustomSignals.Builder();
method @NonNull public com.google.firebase.remoteconfig.CustomSignals build();
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, @Nullable String);
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, long);
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, double);
}

public class FirebaseRemoteConfig {
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Boolean> activate();
method @NonNull public com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration addOnConfigUpdateListener(@NonNull com.google.firebase.remoteconfig.ConfigUpdateListener);
Expand All @@ -35,6 +46,7 @@ package com.google.firebase.remoteconfig {
method @NonNull public com.google.firebase.remoteconfig.FirebaseRemoteConfigValue getValue(@NonNull String);
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> reset();
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setConfigSettingsAsync(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings);
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setCustomSignals(@NonNull com.google.firebase.remoteconfig.CustomSignals);
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setDefaultsAsync(@NonNull java.util.Map<java.lang.String,java.lang.Object>);
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setDefaultsAsync(@XmlRes int);
field public static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.remoteconfig;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;

/**
* Helper class which handles the storage and conversion to strings of key/value pairs with
* heterogeneous value types for custom signals.
*/
public class CustomSignals {

final Map<String, String> customSignals;

public static class Builder {
// Holds the converted pairs of custom keys and values.
private Map<String, String> customSignals = new HashMap<String, String>();

// Methods to accept keys and values and convert values to strings.
@NonNull
public Builder put(@NonNull String key, @Nullable String value) {
customSignals.put(key, value);
return this;
}

@NonNull
public Builder put(@NonNull String key, long value) {
customSignals.put(key, Long.toString(value));
return this;
}

@NonNull
public Builder put(@NonNull String key, double value) {
customSignals.put(key, Double.toString(value));
return this;
}

@NonNull
public CustomSignals build() {
return new CustomSignals(this);
}
}

CustomSignals(@NonNull Builder builder) {
this.customSignals = builder.customSignals;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,25 @@ private Task<Void> setDefaultsWithStringsMapAsync(Map<String, String> defaultsSt
FirebaseExecutors.directExecutor(), (unusedContainer) -> Tasks.forResult(null));
}

/**
* Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance.
*
* <p>The {@code customSignals} parameter should be an instance of {@link CustomSignals}, which
* enforces the allowed types for custom signal values (String, Long or Double).
*
* @param customSignalsMap A dictionary of keys and the values of the custom signals to be set for
* the app instance
*/
@NonNull
public Task<Void> setCustomSignals(@NonNull CustomSignals customSignalsMap) {
return Tasks.call(
executor,
() -> {
frcMetadata.setCustomSignals(customSignalsMap.customSignals);
return null;
});
}

/**
* Notifies the Firebase A/B Testing SDK about activated experiments.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ ConfigFetchHttpClient getFrcBackendApiClient(
apiKey,
namespace,
/* connectTimeoutInSeconds= */ metadataClient.getFetchTimeoutInSeconds(),
/* readTimeoutInSeconds= */ metadataClient.getFetchTimeoutInSeconds());
/* readTimeoutInSeconds= */ metadataClient.getFetchTimeoutInSeconds(),
/* customSignals= */ metadataClient.getCustomSignals());
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public final class RemoteConfigConstants {
RequestFieldKey.PACKAGE_NAME,
RequestFieldKey.SDK_VERSION,
RequestFieldKey.ANALYTICS_USER_PROPERTIES,
RequestFieldKey.FIRST_OPEN_TIME
RequestFieldKey.FIRST_OPEN_TIME,
RequestFieldKey.CUSTOM_SIGNALS
})
@Retention(RetentionPolicy.SOURCE)
public @interface RequestFieldKey {
Expand All @@ -68,6 +69,7 @@ public final class RemoteConfigConstants {
String SDK_VERSION = "sdkVersion";
String ANALYTICS_USER_PROPERTIES = "analyticsUserProperties";
String FIRST_OPEN_TIME = "firstOpenTime";
String CUSTOM_SIGNALS = "customSignals";
}

/** Keys of fields in the Fetch response body from the Firebase Remote Config server. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_ID;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_VERSION;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.COUNTRY_CODE;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.CUSTOM_SIGNALS;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.FIRST_OPEN_TIME;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID_TOKEN;
Expand Down Expand Up @@ -93,6 +94,7 @@ public class ConfigFetchHttpClient {
private final String apiKey;
private final String projectNumber;
private final String namespace;
Map<String, String> customSignalsMap;
private final long connectTimeoutInSeconds;
private final long readTimeoutInSeconds;

Expand All @@ -106,14 +108,16 @@ public ConfigFetchHttpClient(
String apiKey,
String namespace,
long connectTimeoutInSeconds,
long readTimeoutInSeconds) {
long readTimeoutInSeconds,
Map<String, String> customSignalsMap) {
this.context = context;
this.appId = appId;
this.apiKey = apiKey;
this.projectNumber = extractProjectNumberFromAppId(appId);
this.namespace = namespace;
this.connectTimeoutInSeconds = connectTimeoutInSeconds;
this.readTimeoutInSeconds = readTimeoutInSeconds;
this.customSignalsMap = customSignalsMap;
}

/** Used to verify that the timeout is being set correctly. */
Expand Down Expand Up @@ -347,6 +351,10 @@ private JSONObject createFetchRequestBody(

requestBodyMap.put(ANALYTICS_USER_PROPERTIES, new JSONObject(analyticsUserProperties));

if (!customSignalsMap.isEmpty()) {
requestBodyMap.put(CUSTOM_SIGNALS, new JSONObject(customSignalsMap));
}

if (firstOpenTime != null) {
requestBodyMap.put(FIRST_OPEN_TIME, convertToISOString(firstOpenTime));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.LAST_FETCH_STATUS_NO_FETCH_YET;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.LAST_FETCH_STATUS_SUCCESS;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.LAST_FETCH_STATUS_THROTTLED;
import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.TAG;
import static com.google.firebase.remoteconfig.RemoteConfigComponent.CONNECTION_TIMEOUT_IN_SECONDS;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.CUSTOM_SIGNALS;
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
Expand All @@ -31,6 +34,11 @@
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
import java.lang.annotation.Retention;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;

/**
* Client for handling Firebase Remote Config (FRC) metadata that is saved to disk and persisted
Expand Down Expand Up @@ -75,17 +83,26 @@ public class ConfigMetadataClient {
private static final String REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY =
"realtime_backoff_end_time_in_millis";

/** Constants for custom signal limits.*/
private static final int CUSTOM_SIGNALS_MAX_KEY_LENGTH = 250;

private static final int CUSTOM_SIGNALS_MAX_STRING_VALUE_LENGTH = 500;

private static final int CUSTOM_SIGNALS_MAX_COUNT = 100;

private final SharedPreferences frcMetadata;

private final Object frcInfoLock;
private final Object backoffMetadataLock;
private final Object realtimeBackoffMetadataLock;
private final Object customSignalsLock;

public ConfigMetadataClient(SharedPreferences frcMetadata) {
this.frcMetadata = frcMetadata;
this.frcInfoLock = new Object();
this.backoffMetadataLock = new Object();
this.realtimeBackoffMetadataLock = new Object();
this.customSignalsLock = new Object();
}

public long getFetchTimeoutInSeconds() {
Expand Down Expand Up @@ -249,6 +266,72 @@ void setBackoffMetadata(int numFailedFetches, Date backoffEndTime) {
}
}

public void setCustomSignals(Map<String, String> newCustomSignals) {
synchronized (customSignalsLock) {
// Retrieve existing custom signals
Map<String, String> existingCustomSignals = getCustomSignals();

for (Map.Entry<String, String> entry : newCustomSignals.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();

// Validate key and value length
if (key.length() > CUSTOM_SIGNALS_MAX_KEY_LENGTH
|| (value != null && value.length() > CUSTOM_SIGNALS_MAX_STRING_VALUE_LENGTH)) {
Log.w(
TAG,
String.format(
"Invalid custom signal: Custom signal keys must be %d characters or less, and values must be %d characters or less.",
CUSTOM_SIGNALS_MAX_KEY_LENGTH, CUSTOM_SIGNALS_MAX_STRING_VALUE_LENGTH));
return;
}

// Merge new signals with existing ones, overwriting existing keys.
// Also, remove entries where the new value is null.
if (value != null) {
existingCustomSignals.put(key, value);
} else {
existingCustomSignals.remove(key);
}
}

// Check if the map has actually changed and the size limit
if (existingCustomSignals.equals(getCustomSignals())) {
return;
}
if (existingCustomSignals.size() > CUSTOM_SIGNALS_MAX_COUNT) {
Log.w(
TAG,
String.format(
"Invalid custom signal: Too many custom signals provided. The maximum allowed is %d.",
CUSTOM_SIGNALS_MAX_COUNT));
return;
}

frcMetadata
.edit()
.putString(CUSTOM_SIGNALS, new JSONObject(existingCustomSignals).toString())
.commit();
}
}

public Map<String, String> getCustomSignals() {
String jsonString = frcMetadata.getString(CUSTOM_SIGNALS, "{}");
try {
JSONObject existingCustomSignalsJson = new JSONObject(jsonString);
Map<String, String> custom_signals = new HashMap<>();
Iterator<String> keys = existingCustomSignalsJson.keys();
while (keys.hasNext()) {
String key = keys.next();
String value = existingCustomSignalsJson.optString(key);
custom_signals.put(key, value);
}
return custom_signals;
} catch (JSONException e) {
return new HashMap<>();
}
}

void resetBackoff() {
setBackoffMetadata(NO_FAILED_FETCHES, NO_BACKOFF_TIME);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_ID;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_VERSION;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.COUNTRY_CODE;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.CUSTOM_SIGNALS;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.FIRST_OPEN_TIME;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID;
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID_TOKEN;
Expand Down Expand Up @@ -85,6 +86,10 @@ public class ConfigFetchHttpClientTest {
"etag-" + PROJECT_NUMBER + "-" + DEFAULT_NAMESPACE + "-fetch-%d";
private static final String FIRST_ETAG = String.format(ETAG_FORMAT, 1);
private static final String SECOND_ETAG = String.format(ETAG_FORMAT, 2);
private static final Map<String, String> SAMPLE_CUSTOM_SIGNALS =
ImmutableMap.of(
"subscription", "premium",
"age", "20");

private Context context;
private ConfigFetchHttpClient configFetchHttpClient;
Expand All @@ -105,7 +110,8 @@ public void setUp() throws Exception {
API_KEY,
DEFAULT_NAMESPACE,
/* connectTimeoutInSeconds= */ 10L,
/* readTimeoutInSeconds= */ 10L);
/* readTimeoutInSeconds= */ 10L,
/* customSignals= */ SAMPLE_CUSTOM_SIGNALS);

hasChangeResponseBody =
new JSONObject()
Expand Down Expand Up @@ -238,6 +244,8 @@ public void fetch_setsAllElementsOfRequestBody_sendsRequestBodyToServer() throws
assertThat(requestBody.get(FIRST_OPEN_TIME)).isEqualTo(firstOpenTimeIsoString);
assertThat(requestBody.getJSONObject(ANALYTICS_USER_PROPERTIES).toString())
.isEqualTo(new JSONObject(customUserProperties).toString());
assertThat(requestBody.getJSONObject(CUSTOM_SIGNALS).toString())
.isEqualTo(new JSONObject(SAMPLE_CUSTOM_SIGNALS).toString());
}

@Test
Expand Down Expand Up @@ -316,7 +324,8 @@ public void fetch_setsTimeouts_urlConnectionHasTimeouts() throws Exception {
API_KEY,
DEFAULT_NAMESPACE,
/* connectTimeoutInSeconds= */ 15L,
/* readTimeoutInSeconds= */ 20L);
/* readTimeoutInSeconds= */ 20L,
/* customSignals= */ SAMPLE_CUSTOM_SIGNALS);
setServerResponseTo(noChangeResponseBody, SECOND_ETAG);

fetch(FIRST_ETAG);
Expand Down
Loading

0 comments on commit bda1af7

Please sign in to comment.