Skip to content

Commit b5dbd0a

Browse files
Add custom signals support in Remote Config. (#6539)
feat(rc): Add support to set custom signals for Remote Config Custom targeting
1 parent f024090 commit b5dbd0a

25 files changed

+699
-257
lines changed

firebase-config/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Unreleased
2-
2+
* [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.
33

44
# 22.0.1
55
* [changed] Updated protobuf dependency to `3.25.5` to fix

firebase-config/api.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ package com.google.firebase.remoteconfig {
1616
method public void remove();
1717
}
1818

19+
public class CustomSignals {
20+
}
21+
22+
public static class CustomSignals.Builder {
23+
ctor public CustomSignals.Builder();
24+
method @NonNull public com.google.firebase.remoteconfig.CustomSignals build();
25+
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, @Nullable String);
26+
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, long);
27+
method @NonNull public com.google.firebase.remoteconfig.CustomSignals.Builder put(@NonNull String, double);
28+
}
29+
1930
public class FirebaseRemoteConfig {
2031
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Boolean> activate();
2132
method @NonNull public com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration addOnConfigUpdateListener(@NonNull com.google.firebase.remoteconfig.ConfigUpdateListener);
@@ -35,6 +46,7 @@ package com.google.firebase.remoteconfig {
3546
method @NonNull public com.google.firebase.remoteconfig.FirebaseRemoteConfigValue getValue(@NonNull String);
3647
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> reset();
3748
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setConfigSettingsAsync(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings);
49+
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setCustomSignals(@NonNull com.google.firebase.remoteconfig.CustomSignals);
3850
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setDefaultsAsync(@NonNull java.util.Map<java.lang.String,java.lang.Object>);
3951
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> setDefaultsAsync(@XmlRes int);
4052
field public static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false;
@@ -121,6 +133,7 @@ package com.google.firebase.remoteconfig {
121133
}
122134

123135
public final class RemoteConfigKt {
136+
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);
124137
method @NonNull public static operator com.google.firebase.remoteconfig.FirebaseRemoteConfigValue get(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfig, @NonNull String key);
125138
method @NonNull public static kotlinx.coroutines.flow.Flow<com.google.firebase.remoteconfig.ConfigUpdate> getConfigUpdates(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfig);
126139
method @NonNull public static com.google.firebase.remoteconfig.FirebaseRemoteConfig getRemoteConfig(@NonNull com.google.firebase.Firebase);

firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import com.google.firebase.installations.FirebaseInstallationsApi
2323
import com.google.firebase.remoteconfig.internal.ConfigCacheClient
2424
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler
2525
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler
26-
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient
2726
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler
27+
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient
2828
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler
2929
import java.util.concurrent.Executor
3030

@@ -41,7 +41,7 @@ fun createRemoteConfig(
4141
defaultConfigsCache: ConfigCacheClient,
4242
fetchHandler: ConfigFetchHandler,
4343
getHandler: ConfigGetParameterHandler,
44-
frcMetadata: ConfigMetadataClient,
44+
frcSharedPrefs: ConfigSharedPrefsClient,
4545
realtimeHandler: ConfigRealtimeHandler,
4646
rolloutsStateSubscriptionsHandler: RolloutsStateSubscriptionsHandler
4747
): FirebaseRemoteConfig {
@@ -56,7 +56,7 @@ fun createRemoteConfig(
5656
defaultConfigsCache,
5757
fetchHandler,
5858
getHandler,
59-
frcMetadata,
59+
frcSharedPrefs,
6060
realtimeHandler,
6161
rolloutsStateSubscriptionsHandler
6262
)

firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import com.google.firebase.remoteconfig.createRemoteConfig
3232
import com.google.firebase.remoteconfig.internal.ConfigCacheClient
3333
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler
3434
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler
35-
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient
3635
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler
36+
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient
3737
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler
3838
import org.junit.After
3939
import org.junit.Before
@@ -142,7 +142,7 @@ class ConfigTests : BaseTestCase() {
142142
defaultConfigsCache = mock(ConfigCacheClient::class.java),
143143
fetchHandler = mock(ConfigFetchHandler::class.java),
144144
getHandler = mockGetHandler,
145-
frcMetadata = mock(ConfigMetadataClient::class.java),
145+
frcSharedPrefs = mock(ConfigSharedPrefsClient::class.java),
146146
realtimeHandler = mock(ConfigRealtimeHandler::class.java),
147147
rolloutsStateSubscriptionsHandler = mock(RolloutsStateSubscriptionsHandler::class.java)
148148
)

firebase-config/src/androidTest/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIntegrationTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
import com.google.firebase.remoteconfig.internal.ConfigContainer;
3636
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler;
3737
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
38-
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
3938
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler;
39+
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient;
4040
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler;
4141
import java.util.Date;
4242
import java.util.HashMap;
@@ -60,7 +60,7 @@ public class FirebaseRemoteConfigIntegrationTest {
6060
@Mock private ConfigCacheClient mockDefaultsCache;
6161
@Mock private ConfigFetchHandler mockFetchHandler;
6262
@Mock private ConfigGetParameterHandler mockGetHandler;
63-
@Mock private ConfigMetadataClient metadataClient;
63+
@Mock private ConfigSharedPrefsClient sharedPrefsClient;
6464
@Mock private ConfigRealtimeHandler mockConfigRealtimeHandler;
6565
@Mock private RolloutsStateSubscriptionsHandler mockRolloutsStateSubscriptionHandler;
6666

@@ -112,7 +112,7 @@ public void setUp() {
112112
mockDefaultsCache,
113113
mockFetchHandler,
114114
mockGetHandler,
115-
metadataClient,
115+
sharedPrefsClient,
116116
mockConfigRealtimeHandler,
117117
mockRolloutsStateSubscriptionHandler);
118118
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.remoteconfig;
16+
17+
import androidx.annotation.NonNull;
18+
import androidx.annotation.Nullable;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
/**
23+
* A container type to represent key/value pairs of heterogeneous types to be set as custom signals
24+
* in {@link FirebaseRemoteConfig#setCustomSignals}.
25+
*/
26+
public class CustomSignals {
27+
final Map<String, String> customSignals;
28+
29+
/** Builder for constructing {@link CustomSignals} instances. */
30+
public static class Builder {
31+
private Map<String, String> customSignals = new HashMap<String, String>();
32+
33+
/**
34+
* Adds a custom signal with a value that can be a string or null to the builder.
35+
*
36+
* @param key The key for the custom signal.
37+
* @param value The string value associated with the key. Can be null.
38+
* @return This Builder instance to allow chaining of method calls.
39+
*/
40+
@NonNull
41+
public Builder put(@NonNull String key, @Nullable String value) {
42+
customSignals.put(key, value);
43+
return this;
44+
}
45+
46+
/**
47+
* Adds a custom signal with a long value to the builder.
48+
*
49+
* @param key The key for the custom signal.
50+
* @param value The long value for the custom signal.
51+
* @return This Builder instance to allow chaining of method calls.
52+
*/
53+
@NonNull
54+
public Builder put(@NonNull String key, long value) {
55+
customSignals.put(key, Long.toString(value));
56+
return this;
57+
}
58+
59+
/**
60+
* Adds a custom signal with a double value to the builder.
61+
*
62+
* @param key The key for the custom signal.
63+
* @param value The double value for the custom signal.
64+
* @return This Builder instance to allow chaining of method calls.
65+
*/
66+
@NonNull
67+
public Builder put(@NonNull String key, double value) {
68+
customSignals.put(key, Double.toString(value));
69+
return this;
70+
}
71+
72+
/**
73+
* Creates a {@link CustomSignals} instance with the added custom signals.
74+
*
75+
* @return The constructed {@link CustomSignals} instance.
76+
*/
77+
@NonNull
78+
public CustomSignals build() {
79+
return new CustomSignals(this);
80+
}
81+
}
82+
83+
CustomSignals(@NonNull Builder builder) {
84+
this.customSignals = builder.customSignals;
85+
}
86+
}

firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler;
3434
import com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FetchResponse;
3535
import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler;
36-
import com.google.firebase.remoteconfig.internal.ConfigMetadataClient;
3736
import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler;
37+
import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient;
3838
import com.google.firebase.remoteconfig.internal.DefaultsXmlParser;
3939
import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler;
4040
import java.util.ArrayList;
@@ -160,7 +160,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
160160
private final ConfigCacheClient defaultConfigsCache;
161161
private final ConfigFetchHandler fetchHandler;
162162
private final ConfigGetParameterHandler getHandler;
163-
private final ConfigMetadataClient frcMetadata;
163+
private final ConfigSharedPrefsClient frcSharedPrefs;
164164
private final FirebaseInstallationsApi firebaseInstallations;
165165
private final ConfigRealtimeHandler configRealtimeHandler;
166166
private final RolloutsStateSubscriptionsHandler rolloutsStateSubscriptionsHandler;
@@ -181,7 +181,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
181181
ConfigCacheClient defaultConfigsCache,
182182
ConfigFetchHandler fetchHandler,
183183
ConfigGetParameterHandler getHandler,
184-
ConfigMetadataClient frcMetadata,
184+
ConfigSharedPrefsClient frcSharedPrefs,
185185
ConfigRealtimeHandler configRealtimeHandler,
186186
RolloutsStateSubscriptionsHandler rolloutsStateSubscriptionsHandler) {
187187
this.context = context;
@@ -194,7 +194,7 @@ public static FirebaseRemoteConfig getInstance(@NonNull FirebaseApp app) {
194194
this.defaultConfigsCache = defaultConfigsCache;
195195
this.fetchHandler = fetchHandler;
196196
this.getHandler = getHandler;
197-
this.frcMetadata = frcMetadata;
197+
this.frcSharedPrefs = frcSharedPrefs;
198198
this.configRealtimeHandler = configRealtimeHandler;
199199
this.rolloutsStateSubscriptionsHandler = rolloutsStateSubscriptionsHandler;
200200
}
@@ -208,18 +208,18 @@ public Task<FirebaseRemoteConfigInfo> ensureInitialized() {
208208
Task<ConfigContainer> activatedConfigsTask = activatedConfigsCache.get();
209209
Task<ConfigContainer> defaultsConfigsTask = defaultConfigsCache.get();
210210
Task<ConfigContainer> fetchedConfigsTask = fetchedConfigsCache.get();
211-
Task<FirebaseRemoteConfigInfo> metadataTask = Tasks.call(executor, this::getInfo);
211+
Task<FirebaseRemoteConfigInfo> sharedPrefsTask = Tasks.call(executor, this::getInfo);
212212
Task<String> installationIdTask = firebaseInstallations.getId();
213213
Task<InstallationTokenResult> installationTokenTask = firebaseInstallations.getToken(false);
214214

215215
return Tasks.whenAllComplete(
216216
activatedConfigsTask,
217217
defaultsConfigsTask,
218218
fetchedConfigsTask,
219-
metadataTask,
219+
sharedPrefsTask,
220220
installationIdTask,
221221
installationTokenTask)
222-
.continueWith(executor, (unusedListOfCompletedTasks) -> metadataTask.getResult());
222+
.continueWith(executor, (unusedListOfCompletedTasks) -> sharedPrefsTask.getResult());
223223
}
224224

225225
/**
@@ -475,7 +475,7 @@ public Map<String, FirebaseRemoteConfigValue> getAll() {
475475
*/
476476
@NonNull
477477
public FirebaseRemoteConfigInfo getInfo() {
478-
return frcMetadata.getInfo();
478+
return frcSharedPrefs.getInfo();
479479
}
480480

481481
/**
@@ -488,7 +488,7 @@ public Task<Void> setConfigSettingsAsync(@NonNull FirebaseRemoteConfigSettings s
488488
return Tasks.call(
489489
executor,
490490
() -> {
491-
frcMetadata.setConfigSettings(settings);
491+
frcSharedPrefs.setConfigSettings(settings);
492492

493493
// Return value required; return null for Void.
494494
return null;
@@ -548,14 +548,14 @@ public Task<Void> setDefaultsAsync(@XmlRes int resourceId) {
548548
@NonNull
549549
public Task<Void> reset() {
550550
// Use a Task to avoid throwing potential file I/O errors to the caller and because
551-
// frcMetadata's clear call is blocking.
551+
// frcSharedPrefs's clear call is blocking.
552552
return Tasks.call(
553553
executor,
554554
() -> {
555555
activatedConfigsCache.clear();
556556
fetchedConfigsCache.clear();
557557
defaultConfigsCache.clear();
558-
frcMetadata.clear();
558+
frcSharedPrefs.clear();
559559
return null;
560560
});
561561
}
@@ -652,6 +652,30 @@ private Task<Void> setDefaultsWithStringsMapAsync(Map<String, String> defaultsSt
652652
FirebaseExecutors.directExecutor(), (unusedContainer) -> Tasks.forResult(null));
653653
}
654654

655+
/**
656+
* Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance.
657+
*
658+
* <p>Custom signals are subject to limits on the size of key/value pairs and the total
659+
* number of signals. Any calls that exceed these limits will be discarded.
660+
*
661+
* @param customSignals The custom signals to set for this instance.
662+
* <ol>
663+
* <li>New keys will add new key-value pairs in the custom signals.
664+
* <li>Existing keys with new values will update the corresponding signals.
665+
* <li>Setting a key's value to {@code null} will remove the associated signal.
666+
* </ol>
667+
*/
668+
// TODO(b/385028620): Add link to documentation about custom signal limits.
669+
@NonNull
670+
public Task<Void> setCustomSignals(@NonNull CustomSignals customSignals) {
671+
return Tasks.call(
672+
executor,
673+
() -> {
674+
frcSharedPrefs.setCustomSignals(customSignals.customSignals);
675+
return null;
676+
});
677+
}
678+
655679
/**
656680
* Notifies the Firebase A/B Testing SDK about activated experiments.
657681
*

firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ fun remoteConfigSettings(
4848
return builder.build()
4949
}
5050

51+
fun customSignals(builder: CustomSignals.Builder.() -> Unit) =
52+
CustomSignals.Builder().apply(builder).build()
53+
5154
/**
5255
* Starts listening for config updates from the Remote Config backend and emits [ConfigUpdate]s via
5356
* a [Flow]. See [FirebaseRemoteConfig.addOnConfigUpdateListener] for more information.

0 commit comments

Comments
 (0)