Skip to content

Commit

Permalink
Add custom signals support in Remote Config. (#6539)
Browse files Browse the repository at this point in the history
feat(rc): Add support to set custom signals for Remote Config Custom
targeting
  • Loading branch information
tusharkhandelwal8 authored Jan 8, 2025
1 parent f024090 commit b5dbd0a
Show file tree
Hide file tree
Showing 25 changed files with 699 additions and 257 deletions.
2 changes: 1 addition & 1 deletion firebase-config/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Unreleased

* [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config.

# 22.0.1
* [changed] Updated protobuf dependency to `3.25.5` to fix
Expand Down
13 changes: 13 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 Expand Up @@ -121,6 +133,7 @@ package com.google.firebase.remoteconfig {
}

public final class RemoteConfigKt {
method @NonNull public static com.google.firebase.remoteconfig.CustomSignals customSignals(@NonNull kotlin.jvm.functions.Function1<? super com.google.firebase.remoteconfig.CustomSignals.Builder,kotlin.Unit> builder);
method @NonNull public static operator com.google.firebase.remoteconfig.FirebaseRemoteConfigValue get(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfig, @NonNull String key);
method @NonNull public static kotlinx.coroutines.flow.Flow<com.google.firebase.remoteconfig.ConfigUpdate> getConfigUpdates(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfig);
method @NonNull public static com.google.firebase.remoteconfig.FirebaseRemoteConfig getRemoteConfig(@NonNull com.google.firebase.Firebase);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import com.google.firebase.installations.FirebaseInstallationsApi
import com.google.firebase.remoteconfig.internal.ConfigCacheClient
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler
import java.util.concurrent.Executor

Expand All @@ -41,7 +41,7 @@ fun createRemoteConfig(
defaultConfigsCache: ConfigCacheClient,
fetchHandler: ConfigFetchHandler,
getHandler: ConfigGetParameterHandler,
frcMetadata: ConfigMetadataClient,
frcSharedPrefs: ConfigSharedPrefsClient,
realtimeHandler: ConfigRealtimeHandler,
rolloutsStateSubscriptionsHandler: RolloutsStateSubscriptionsHandler
): FirebaseRemoteConfig {
Expand All @@ -56,7 +56,7 @@ fun createRemoteConfig(
defaultConfigsCache,
fetchHandler,
getHandler,
frcMetadata,
frcSharedPrefs,
realtimeHandler,
rolloutsStateSubscriptionsHandler
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import com.google.firebase.remoteconfig.createRemoteConfig
import com.google.firebase.remoteconfig.internal.ConfigCacheClient
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler
import org.junit.After
import org.junit.Before
Expand Down Expand Up @@ -142,7 +142,7 @@ class ConfigTests : BaseTestCase() {
defaultConfigsCache = mock(ConfigCacheClient::class.java),
fetchHandler = mock(ConfigFetchHandler::class.java),
getHandler = mockGetHandler,
frcMetadata = mock(ConfigMetadataClient::class.java),
frcSharedPrefs = mock(ConfigSharedPrefsClient::class.java),
realtimeHandler = mock(ConfigRealtimeHandler::class.java),
rolloutsStateSubscriptionsHandler = mock(RolloutsStateSubscriptionsHandler::class.java)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
import com.google.firebase.remoteconfig.internal.ConfigContainer;
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler;
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler;
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient;
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler;
import java.util.Date;
import java.util.HashMap;
Expand All @@ -60,7 +60,7 @@ public class FirebaseRemoteConfigIntegrationTest {
@Mock private ConfigCacheClient mockDefaultsCache;
@Mock private ConfigFetchHandler mockFetchHandler;
@Mock private ConfigGetParameterHandler mockGetHandler;
@Mock private ConfigMetadataClient metadataClient;
@Mock private ConfigSharedPrefsClient sharedPrefsClient;
@Mock private ConfigRealtimeHandler mockConfigRealtimeHandler;
@Mock private RolloutsStateSubscriptionsHandler mockRolloutsStateSubscriptionHandler;

Expand Down Expand Up @@ -112,7 +112,7 @@ public void setUp() {
mockDefaultsCache,
mockFetchHandler,
mockGetHandler,
metadataClient,
sharedPrefsClient,
mockConfigRealtimeHandler,
mockRolloutsStateSubscriptionHandler);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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;

/**
* A container type to represent key/value pairs of heterogeneous types to be set as custom signals
* in {@link FirebaseRemoteConfig#setCustomSignals}.
*/
public class CustomSignals {
final Map<String, String> customSignals;

/** Builder for constructing {@link CustomSignals} instances. */
public static class Builder {
private Map<String, String> customSignals = new HashMap<String, String>();

/**
* Adds a custom signal with a value that can be a string or null to the builder.
*
* @param key The key for the custom signal.
* @param value The string value associated with the key. Can be null.
* @return This Builder instance to allow chaining of method calls.
*/
@NonNull
public Builder put(@NonNull String key, @Nullable String value) {
customSignals.put(key, value);
return this;
}

/**
* Adds a custom signal with a long value to the builder.
*
* @param key The key for the custom signal.
* @param value The long value for the custom signal.
* @return This Builder instance to allow chaining of method calls.
*/
@NonNull
public Builder put(@NonNull String key, long value) {
customSignals.put(key, Long.toString(value));
return this;
}

/**
* Adds a custom signal with a double value to the builder.
*
* @param key The key for the custom signal.
* @param value The double value for the custom signal.
* @return This Builder instance to allow chaining of method calls.
*/
@NonNull
public Builder put(@NonNull String key, double value) {
customSignals.put(key, Double.toString(value));
return this;
}

/**
* Creates a {@link CustomSignals} instance with the added custom signals.
*
* @return The constructed {@link CustomSignals} instance.
*/
@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 @@ -33,8 +33,8 @@
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler;
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FetchResponse;
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler;
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient;
import com.google.firebase.remoteconfig.internal.DefaultsXmlParser;
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler;
import java.util.ArrayList;
Expand Down Expand Up @@ -160,7 +160,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
private final ConfigCacheClient defaultConfigsCache;
private final ConfigFetchHandler fetchHandler;
private final ConfigGetParameterHandler getHandler;
private final ConfigMetadataClient frcMetadata;
private final ConfigSharedPrefsClient frcSharedPrefs;
private final FirebaseInstallationsApi firebaseInstallations;
private final ConfigRealtimeHandler configRealtimeHandler;
private final RolloutsStateSubscriptionsHandler rolloutsStateSubscriptionsHandler;
Expand All @@ -181,7 +181,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
ConfigCacheClient defaultConfigsCache,
ConfigFetchHandler fetchHandler,
ConfigGetParameterHandler getHandler,
ConfigMetadataClient frcMetadata,
ConfigSharedPrefsClient frcSharedPrefs,
ConfigRealtimeHandler configRealtimeHandler,
RolloutsStateSubscriptionsHandler rolloutsStateSubscriptionsHandler) {
this.context = context;
Expand All @@ -194,7 +194,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
this.defaultConfigsCache = defaultConfigsCache;
this.fetchHandler = fetchHandler;
this.getHandler = getHandler;
this.frcMetadata = frcMetadata;
this.frcSharedPrefs = frcSharedPrefs;
this.configRealtimeHandler = configRealtimeHandler;
this.rolloutsStateSubscriptionsHandler = rolloutsStateSubscriptionsHandler;
}
Expand All @@ -208,18 +208,18 @@ public Task<FirebaseRemoteConfigInfo> ensureInitialized() {
Task<ConfigContainer> activatedConfigsTask = activatedConfigsCache.get();
Task<ConfigContainer> defaultsConfigsTask = defaultConfigsCache.get();
Task<ConfigContainer> fetchedConfigsTask = fetchedConfigsCache.get();
Task<FirebaseRemoteConfigInfo> metadataTask = Tasks.call(executor, this::getInfo);
Task<FirebaseRemoteConfigInfo> sharedPrefsTask = Tasks.call(executor, this::getInfo);
Task<String> installationIdTask = firebaseInstallations.getId();
Task<InstallationTokenResult> installationTokenTask = firebaseInstallations.getToken(false);

return Tasks.whenAllComplete(
activatedConfigsTask,
defaultsConfigsTask,
fetchedConfigsTask,
metadataTask,
sharedPrefsTask,
installationIdTask,
installationTokenTask)
.continueWith(executor, (unusedListOfCompletedTasks) -> metadataTask.getResult());
.continueWith(executor, (unusedListOfCompletedTasks) -> sharedPrefsTask.getResult());
}

/**
Expand Down Expand Up @@ -475,7 +475,7 @@ public Map<String, FirebaseRemoteConfigValue> getAll() {
*/
@NonNull
public FirebaseRemoteConfigInfo getInfo() {
return frcMetadata.getInfo();
return frcSharedPrefs.getInfo();
}

/**
Expand All @@ -488,7 +488,7 @@ public Task<Void> setConfigSettingsAsync(@NonNull FirebaseRemoteConfigSettings s
return Tasks.call(
executor,
() -> {
frcMetadata.setConfigSettings(settings);
frcSharedPrefs.setConfigSettings(settings);

// Return value required; return null for Void.
return null;
Expand Down Expand Up @@ -548,14 +548,14 @@ public Task<Void> setDefaultsAsync(@XmlRes int resourceId) {
@NonNull
public Task<Void> reset() {
// Use a Task to avoid throwing potential file I/O errors to the caller and because
// frcMetadata's clear call is blocking.
// frcSharedPrefs's clear call is blocking.
return Tasks.call(
executor,
() -> {
activatedConfigsCache.clear();
fetchedConfigsCache.clear();
defaultConfigsCache.clear();
frcMetadata.clear();
frcSharedPrefs.clear();
return null;
});
}
Expand Down Expand Up @@ -652,6 +652,30 @@ 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>Custom signals are subject to limits on the size of key/value pairs and the total
* number of signals. Any calls that exceed these limits will be discarded.
*
* @param customSignals The custom signals to set for this instance.
* <ol>
* <li>New keys will add new key-value pairs in the custom signals.
* <li>Existing keys with new values will update the corresponding signals.
* <li>Setting a key's value to {@code null} will remove the associated signal.
* </ol>
*/
// TODO(b/385028620): Add link to documentation about custom signal limits.
@NonNull
public Task<Void> setCustomSignals(@NonNull CustomSignals customSignals) {
return Tasks.call(
executor,
() -> {
frcSharedPrefs.setCustomSignals(customSignals.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 @@ -48,6 +48,9 @@ fun remoteConfigSettings(
return builder.build()
}

fun customSignals(builder: CustomSignals.Builder.() -> Unit) =
CustomSignals.Builder().apply(builder).build()

/**
* Starts listening for config updates from the Remote Config backend and emits [ConfigUpdate]s via
* a [Flow]. See [FirebaseRemoteConfig.addOnConfigUpdateListener] for more information.
Expand Down
Loading

0 comments on commit b5dbd0a

Please sign in to comment.