diff --git a/firebase-config/CHANGELOG.md b/firebase-config/CHANGELOG.md index edf65709ccd..29ca82b2238 100644 --- a/firebase-config/CHANGELOG.md +++ b/firebase-config/CHANGELOG.md @@ -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 diff --git a/firebase-config/api.txt b/firebase-config/api.txt index 61528ec5d11..58e75c9e18c 100644 --- a/firebase-config/api.txt +++ b/firebase-config/api.txt @@ -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 activate(); method @NonNull public com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration addOnConfigUpdateListener(@NonNull com.google.firebase.remoteconfig.ConfigUpdateListener); @@ -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 reset(); method @NonNull public com.google.android.gms.tasks.Task setConfigSettingsAsync(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings); + method @NonNull public com.google.android.gms.tasks.Task setCustomSignals(@NonNull com.google.firebase.remoteconfig.CustomSignals); method @NonNull public com.google.android.gms.tasks.Task setDefaultsAsync(@NonNull java.util.Map); method @NonNull public com.google.android.gms.tasks.Task setDefaultsAsync(@XmlRes int); field public static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false; @@ -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 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 getConfigUpdates(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfig); method @NonNull public static com.google.firebase.remoteconfig.FirebaseRemoteConfig getRemoteConfig(@NonNull com.google.firebase.Firebase); diff --git a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt index e6167b5ab09..4c153bff1e3 100644 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt +++ b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt @@ -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 @@ -41,7 +41,7 @@ fun createRemoteConfig( defaultConfigsCache: ConfigCacheClient, fetchHandler: ConfigFetchHandler, getHandler: ConfigGetParameterHandler, - frcMetadata: ConfigMetadataClient, + frcSharedPrefs: ConfigSharedPrefsClient, realtimeHandler: ConfigRealtimeHandler, rolloutsStateSubscriptionsHandler: RolloutsStateSubscriptionsHandler ): FirebaseRemoteConfig { @@ -56,7 +56,7 @@ fun createRemoteConfig( defaultConfigsCache, fetchHandler, getHandler, - frcMetadata, + frcSharedPrefs, realtimeHandler, rolloutsStateSubscriptionsHandler ) diff --git a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt index bb525cf4090..d3e7d10e725 100644 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt +++ b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt @@ -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 @@ -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) ) diff --git a/firebase-config/src/androidTest/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIntegrationTest.java b/firebase-config/src/androidTest/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIntegrationTest.java index 19900986b6f..cc8c259836e 100644 --- a/firebase-config/src/androidTest/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIntegrationTest.java +++ b/firebase-config/src/androidTest/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigIntegrationTest.java @@ -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; @@ -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; @@ -112,7 +112,7 @@ public void setUp() { mockDefaultsCache, mockFetchHandler, mockGetHandler, - metadataClient, + sharedPrefsClient, mockConfigRealtimeHandler, mockRolloutsStateSubscriptionHandler); } diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/CustomSignals.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/CustomSignals.java new file mode 100644 index 00000000000..78d8d6cc415 --- /dev/null +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/CustomSignals.java @@ -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 customSignals; + + /** Builder for constructing {@link CustomSignals} instances. */ + public static class Builder { + private Map customSignals = new HashMap(); + + /** + * 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; + } +} diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index b2e7e73d954..808892e7521 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -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; @@ -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; @@ -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; @@ -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; } @@ -208,7 +208,7 @@ public Task ensureInitialized() { Task activatedConfigsTask = activatedConfigsCache.get(); Task defaultsConfigsTask = defaultConfigsCache.get(); Task fetchedConfigsTask = fetchedConfigsCache.get(); - Task metadataTask = Tasks.call(executor, this::getInfo); + Task sharedPrefsTask = Tasks.call(executor, this::getInfo); Task installationIdTask = firebaseInstallations.getId(); Task installationTokenTask = firebaseInstallations.getToken(false); @@ -216,10 +216,10 @@ public Task ensureInitialized() { activatedConfigsTask, defaultsConfigsTask, fetchedConfigsTask, - metadataTask, + sharedPrefsTask, installationIdTask, installationTokenTask) - .continueWith(executor, (unusedListOfCompletedTasks) -> metadataTask.getResult()); + .continueWith(executor, (unusedListOfCompletedTasks) -> sharedPrefsTask.getResult()); } /** @@ -475,7 +475,7 @@ public Map getAll() { */ @NonNull public FirebaseRemoteConfigInfo getInfo() { - return frcMetadata.getInfo(); + return frcSharedPrefs.getInfo(); } /** @@ -488,7 +488,7 @@ public Task setConfigSettingsAsync(@NonNull FirebaseRemoteConfigSettings s return Tasks.call( executor, () -> { - frcMetadata.setConfigSettings(settings); + frcSharedPrefs.setConfigSettings(settings); // Return value required; return null for Void. return null; @@ -548,14 +548,14 @@ public Task setDefaultsAsync(@XmlRes int resourceId) { @NonNull public Task 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; }); } @@ -652,6 +652,30 @@ private Task setDefaultsWithStringsMapAsync(Map defaultsSt FirebaseExecutors.directExecutor(), (unusedContainer) -> Tasks.forResult(null)); } + /** + * Asynchronously changes the custom signals for this {@link FirebaseRemoteConfig} instance. + * + *

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. + *

    + *
  1. New keys will add new key-value pairs in the custom signals. + *
  2. Existing keys with new values will update the corresponding signals. + *
  3. Setting a key's value to {@code null} will remove the associated signal. + *
+ */ + // TODO(b/385028620): Add link to documentation about custom signal limits. + @NonNull + public Task setCustomSignals(@NonNull CustomSignals customSignals) { + return Tasks.call( + executor, + () -> { + frcSharedPrefs.setCustomSignals(customSignals.customSignals); + return null; + }); + } + /** * Notifies the Firebase A/B Testing SDK about activated experiments. * diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfig.kt b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfig.kt index a944a7e8857..3a7ef220198 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfig.kt +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfig.kt @@ -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. diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java index 0835488f934..73fcec6e6c0 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java @@ -36,8 +36,8 @@ import com.google.firebase.remoteconfig.internal.ConfigFetchHandler; import com.google.firebase.remoteconfig.internal.ConfigFetchHttpClient; 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.ConfigStorageClient; import com.google.firebase.remoteconfig.internal.Personalization; import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateFactory; @@ -166,7 +166,7 @@ public synchronized FirebaseRemoteConfig get(String namespace) { ConfigCacheClient fetchedCacheClient = getCacheClient(namespace, FETCH_FILE_NAME); ConfigCacheClient activatedCacheClient = getCacheClient(namespace, ACTIVATE_FILE_NAME); ConfigCacheClient defaultsCacheClient = getCacheClient(namespace, DEFAULTS_FILE_NAME); - ConfigMetadataClient metadataClient = getMetadataClient(context, appId, namespace); + ConfigSharedPrefsClient sharedPrefsClient = getSharedPrefsClient(context, appId, namespace); ConfigGetParameterHandler getHandler = getGetHandler(activatedCacheClient, defaultsCacheClient); Personalization personalization = @@ -187,9 +187,9 @@ public synchronized FirebaseRemoteConfig get(String namespace) { fetchedCacheClient, activatedCacheClient, defaultsCacheClient, - getFetchHandler(namespace, fetchedCacheClient, metadataClient), + getFetchHandler(namespace, fetchedCacheClient, sharedPrefsClient), getHandler, - metadataClient, + sharedPrefsClient, rolloutsStateSubscriptionsHandler); } @@ -206,7 +206,7 @@ synchronized FirebaseRemoteConfig get( ConfigCacheClient defaultsClient, ConfigFetchHandler fetchHandler, ConfigGetParameterHandler getHandler, - ConfigMetadataClient metadataClient, + ConfigSharedPrefsClient sharedPrefsClient, RolloutsStateSubscriptionsHandler rolloutsStateSubscriptionsHandler) { if (!frcNamespaceInstances.containsKey(namespace)) { FirebaseRemoteConfig in = @@ -221,7 +221,7 @@ synchronized FirebaseRemoteConfig get( defaultsClient, fetchHandler, getHandler, - metadataClient, + sharedPrefsClient, getRealtime( firebaseApp, firebaseInstallations, @@ -229,7 +229,7 @@ synchronized FirebaseRemoteConfig get( activatedClient, context, namespace, - metadataClient), + sharedPrefsClient), rolloutsStateSubscriptionsHandler); in.startLoadingConfigsFromDisk(); frcNamespaceInstances.put(namespace, in); @@ -254,20 +254,22 @@ private ConfigCacheClient getCacheClient(String namespace, String configStoreTyp @VisibleForTesting ConfigFetchHttpClient getFrcBackendApiClient( - String apiKey, String namespace, ConfigMetadataClient metadataClient) { + String apiKey, String namespace, ConfigSharedPrefsClient sharedPrefsClient) { String appId = firebaseApp.getOptions().getApplicationId(); return new ConfigFetchHttpClient( context, appId, apiKey, namespace, - /* connectTimeoutInSeconds= */ metadataClient.getFetchTimeoutInSeconds(), - /* readTimeoutInSeconds= */ metadataClient.getFetchTimeoutInSeconds()); + /* connectTimeoutInSeconds= */ sharedPrefsClient.getFetchTimeoutInSeconds(), + /* readTimeoutInSeconds= */ sharedPrefsClient.getFetchTimeoutInSeconds()); } @VisibleForTesting synchronized ConfigFetchHandler getFetchHandler( - String namespace, ConfigCacheClient fetchedCacheClient, ConfigMetadataClient metadataClient) { + String namespace, + ConfigCacheClient fetchedCacheClient, + ConfigSharedPrefsClient sharedPrefsClient) { return new ConfigFetchHandler( firebaseInstallations, isPrimaryApp(firebaseApp) ? analyticsConnector : () -> null, @@ -275,8 +277,8 @@ synchronized ConfigFetchHandler getFetchHandler( DEFAULT_CLOCK, DEFAULT_RANDOM, fetchedCacheClient, - getFrcBackendApiClient(firebaseApp.getOptions().getApiKey(), namespace, metadataClient), - metadataClient, + getFrcBackendApiClient(firebaseApp.getOptions().getApiKey(), namespace, sharedPrefsClient), + sharedPrefsClient, this.customHeaders); } @@ -287,7 +289,7 @@ synchronized ConfigRealtimeHandler getRealtime( ConfigCacheClient activatedCacheClient, Context context, String namespace, - ConfigMetadataClient metadataClient) { + ConfigSharedPrefsClient sharedPrefsClient) { return new ConfigRealtimeHandler( firebaseApp, firebaseInstallations, @@ -295,7 +297,7 @@ synchronized ConfigRealtimeHandler getRealtime( activatedCacheClient, context, namespace, - metadataClient, + sharedPrefsClient, executor); } @@ -305,13 +307,14 @@ private ConfigGetParameterHandler getGetHandler( } @VisibleForTesting - static ConfigMetadataClient getMetadataClient(Context context, String appId, String namespace) { + static ConfigSharedPrefsClient getSharedPrefsClient( + Context context, String appId, String namespace) { String fileName = String.format( "%s_%s_%s_%s", FIREBASE_REMOTE_CONFIG_FILE_NAME_PREFIX, appId, namespace, PREFERENCES_FILE_NAME); SharedPreferences preferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE); - return new ConfigMetadataClient(preferences); + return new ConfigSharedPrefsClient(preferences); } @Nullable diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigConstants.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigConstants.java index 9c1a352dd0a..410306afd9c 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigConstants.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigConstants.java @@ -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 { @@ -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. */ diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java index 7c98589b153..e130d13df49 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java @@ -14,7 +14,7 @@ package com.google.firebase.remoteconfig.internal; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LAST_FETCH_TIME_NO_FETCH_YET; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LAST_FETCH_TIME_NO_FETCH_YET; import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT; @@ -43,7 +43,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigFetchThrottledException; import com.google.firebase.remoteconfig.FirebaseRemoteConfigServerException; import com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FetchResponse.Status; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient.BackoffMetadata; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.BackoffMetadata; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.HttpURLConnection; @@ -96,7 +96,7 @@ public class ConfigFetchHandler { private final Random randomGenerator; private final ConfigCacheClient fetchedConfigsCache; private final ConfigFetchHttpClient frcBackendApiClient; - private final ConfigMetadataClient frcMetadata; + private final ConfigSharedPrefsClient frcSharedPrefs; private final Map customHttpHeaders; @@ -109,7 +109,7 @@ public ConfigFetchHandler( Random randomGenerator, ConfigCacheClient fetchedConfigsCache, ConfigFetchHttpClient frcBackendApiClient, - ConfigMetadataClient frcMetadata, + ConfigSharedPrefsClient frcSharedPrefs, Map customHttpHeaders) { this.firebaseInstallations = firebaseInstallations; this.analyticsConnector = analyticsConnector; @@ -118,16 +118,16 @@ public ConfigFetchHandler( this.randomGenerator = randomGenerator; this.fetchedConfigsCache = fetchedConfigsCache; this.frcBackendApiClient = frcBackendApiClient; - this.frcMetadata = frcMetadata; + this.frcSharedPrefs = frcSharedPrefs; this.customHttpHeaders = customHttpHeaders; } /** * Calls {@link #fetch(long)} with the {@link - * ConfigMetadataClient#getMinimumFetchIntervalInSeconds()}. + * ConfigSharedPrefsClient#getMinimumFetchIntervalInSeconds()}. */ public Task fetch() { - return fetch(frcMetadata.getMinimumFetchIntervalInSeconds()); + return fetch(frcSharedPrefs.getMinimumFetchIntervalInSeconds()); } /** @@ -228,7 +228,7 @@ public Task fetchNowWithTypeAndAttemptNumber( * currently throttled. * *

If a fetch request is made to the backend, updates the last fetch status, last successful - * fetch time and {@link BackoffMetadata} in {@link ConfigMetadataClient}. + * fetch time and {@link BackoffMetadata} in {@link ConfigSharedPrefsClient}. */ private Task fetchIfCacheExpiredAndNotThrottled( Task cachedFetchConfigsTask, @@ -295,7 +295,7 @@ && areCachedFetchConfigsValid(minimumFetchIntervalInSeconds, currentTime)) { * on. */ private boolean areCachedFetchConfigsValid(long cacheExpirationInSeconds, Date newFetchTime) { - Date lastSuccessfulFetchTime = frcMetadata.getLastSuccessfulFetchTime(); + Date lastSuccessfulFetchTime = frcSharedPrefs.getLastSuccessfulFetchTime(); // RC always fetches if the client has not previously had a successful fetch. if (lastSuccessfulFetchTime.equals(LAST_FETCH_TIME_NO_FETCH_YET)) { @@ -315,7 +315,7 @@ private boolean areCachedFetchConfigsValid(long cacheExpirationInSeconds, Date n */ @Nullable private Date getBackoffEndTimeInMillis(Date currentTime) { - Date backoffEndTime = frcMetadata.getBackoffMetadata().getBackoffEndTime(); + Date backoffEndTime = frcSharedPrefs.getBackoffMetadata().getBackoffEndTime(); if (currentTime.before(backoffEndTime)) { return backoffEndTime; } @@ -381,21 +381,23 @@ private FetchResponse fetchFromBackend( installationId, installationToken, getUserProperties(), - frcMetadata.getLastFetchETag(), + frcSharedPrefs.getLastFetchETag(), customFetchHeaders, getFirstOpenTime(), - currentTime); + currentTime, + frcSharedPrefs.getCustomSignals()); if (response.getFetchedConfigs() != null) { // Set template version in metadata to be saved on disk. - frcMetadata.setLastTemplateVersion(response.getFetchedConfigs().getTemplateVersionNumber()); + frcSharedPrefs.setLastTemplateVersion( + response.getFetchedConfigs().getTemplateVersionNumber()); } if (response.getLastFetchETag() != null) { - frcMetadata.setLastFetchETag(response.getLastFetchETag()); + frcSharedPrefs.setLastFetchETag(response.getLastFetchETag()); } // If the execute method did not throw exceptions, then the server sent a successful response // and the client can stop backing off. - frcMetadata.resetBackoff(); + frcSharedPrefs.resetBackoff(); return response; } catch (FirebaseRemoteConfigServerException serverHttpError) { @@ -473,7 +475,7 @@ private BackoffMetadata updateAndReturnBackoffMetadata(int statusCode, Date curr if (isThrottleableServerError(statusCode)) { updateBackoffMetadataWithLastFailedFetchTime(currentTime); } - return frcMetadata.getBackoffMetadata(); + return frcSharedPrefs.getBackoffMetadata(); } /** @@ -497,14 +499,14 @@ private boolean isThrottleableServerError(int httpStatusCode) { * disk-backed metadata. */ private void updateBackoffMetadataWithLastFailedFetchTime(Date lastFailedFetchTime) { - int numFailedFetches = frcMetadata.getBackoffMetadata().getNumFailedFetches(); + int numFailedFetches = frcSharedPrefs.getBackoffMetadata().getNumFailedFetches(); numFailedFetches++; long backoffDurationInMillis = getRandomizedBackoffDurationInMillis(numFailedFetches); Date backoffEndTime = new Date(lastFailedFetchTime.getTime() + backoffDurationInMillis); - frcMetadata.setBackoffMetadata(numFailedFetches, backoffEndTime); + frcSharedPrefs.setBackoffMetadata(numFailedFetches, backoffEndTime); } /** @@ -551,7 +553,7 @@ private boolean shouldThrottle(BackoffMetadata backoffMetadata, int httpStatusCo private void updateLastFetchStatusAndTime( Task completedFetchTask, Date fetchTime) { if (completedFetchTask.isSuccessful()) { - frcMetadata.updateLastFetchAsSuccessfulAt(fetchTime); + frcSharedPrefs.updateLastFetchAsSuccessfulAt(fetchTime); return; } @@ -562,9 +564,9 @@ private void updateLastFetchStatusAndTime( } if (fetchException instanceof FirebaseRemoteConfigFetchThrottledException) { - frcMetadata.updateLastFetchAsThrottled(); + frcSharedPrefs.updateLastFetchAsThrottled(); } else { - frcMetadata.updateLastFetchAsFailed(); + frcSharedPrefs.updateLastFetchAsFailed(); } } @@ -602,7 +604,7 @@ private Long getFirstOpenTime() { } public long getTemplateVersionNumber() { - return frcMetadata.getLastTemplateVersion(); + return frcSharedPrefs.getLastTemplateVersion(); } /** Used to verify that the fetch handler is getting Analytics as expected. */ diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClient.java index b1867347580..ab068ffc674 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClient.java @@ -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; @@ -183,7 +184,8 @@ FetchResponse fetch( String lastFetchETag, Map customHeaders, Long firstOpenTime, - Date currentTime) + Date currentTime, + Map customSignalsMap) throws FirebaseRemoteConfigException { setUpUrlConnection(urlConnection, lastFetchETag, installationAuthToken, customHeaders); @@ -192,7 +194,11 @@ FetchResponse fetch( try { byte[] requestBody = createFetchRequestBody( - installationId, installationAuthToken, analyticsUserProperties, firstOpenTime) + installationId, + installationAuthToken, + analyticsUserProperties, + firstOpenTime, + customSignalsMap) .toString() .getBytes("utf-8"); setFetchRequestBody(urlConnection, requestBody); @@ -303,7 +309,8 @@ private JSONObject createFetchRequestBody( String installationId, String installationAuthToken, Map analyticsUserProperties, - Long firstOpenTime) + Long firstOpenTime, + Map customSignalsMap) throws FirebaseRemoteConfigClientException { Map requestBodyMap = new HashMap<>(); @@ -347,6 +354,13 @@ private JSONObject createFetchRequestBody( requestBodyMap.put(ANALYTICS_USER_PROPERTIES, new JSONObject(analyticsUserProperties)); + if (!customSignalsMap.isEmpty()) { + requestBodyMap.put(CUSTOM_SIGNALS, new JSONObject(customSignalsMap)); + + // Log the keys of the custom signals sent during fetch. + Log.d(TAG, "Keys of custom signals during fetch: " + customSignalsMap.keySet()); + } + if (firstOpenTime != null) { requestBodyMap.put(FIRST_OPEN_TIME, convertToISOString(firstOpenTime)); } diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java index 53f89a71e8c..5ed1135dfc7 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java @@ -39,7 +39,7 @@ public class ConfigRealtimeHandler { private final ConfigCacheClient activatedCacheClient; private final Context context; private final String namespace; - private final ConfigMetadataClient metadataClient; + private final ConfigSharedPrefsClient sharedPrefsClient; private final ScheduledExecutorService scheduledExecutorService; public ConfigRealtimeHandler( @@ -49,7 +49,7 @@ public ConfigRealtimeHandler( ConfigCacheClient activatedCacheClient, Context context, String namespace, - ConfigMetadataClient metadataClient, + ConfigSharedPrefsClient sharedPrefsClient, ScheduledExecutorService scheduledExecutorService) { this.listeners = new LinkedHashSet<>(); @@ -62,7 +62,7 @@ public ConfigRealtimeHandler( context, namespace, listeners, - metadataClient, + sharedPrefsClient, scheduledExecutorService); this.firebaseApp = firebaseApp; @@ -71,7 +71,7 @@ public ConfigRealtimeHandler( this.activatedCacheClient = activatedCacheClient; this.context = context; this.namespace = namespace; - this.metadataClient = metadataClient; + this.sharedPrefsClient = sharedPrefsClient; this.scheduledExecutorService = scheduledExecutorService; } diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java index ef0ef2defc5..2c1c44480e2 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java @@ -110,7 +110,7 @@ public class ConfigRealtimeHttpClient { private final String namespace; private final Random random; private final Clock clock; - private final ConfigMetadataClient metadataClient; + private final ConfigSharedPrefsClient sharedPrefsClient; public ConfigRealtimeHttpClient( FirebaseApp firebaseApp, @@ -120,7 +120,7 @@ public ConfigRealtimeHttpClient( Context context, String namespace, Set listeners, - ConfigMetadataClient metadataClient, + ConfigSharedPrefsClient sharedPrefsClient, ScheduledExecutorService scheduledExecutorService) { this.listeners = listeners; @@ -132,7 +132,7 @@ public ConfigRealtimeHttpClient( // Retrieve number of remaining retries from last session. The minimum retry count being one. httpRetriesRemaining = Math.max( - ORIGINAL_RETRIES - metadataClient.getRealtimeBackoffMetadata().getNumFailedStreams(), + ORIGINAL_RETRIES - sharedPrefsClient.getRealtimeBackoffMetadata().getNumFailedStreams(), 1); clock = DefaultClock.getInstance(); @@ -142,7 +142,7 @@ public ConfigRealtimeHttpClient( this.activatedCache = activatedCache; this.context = context; this.namespace = namespace; - this.metadataClient = metadataClient; + this.sharedPrefsClient = sharedPrefsClient; this.isRealtimeDisabled = false; this.isInBackground = false; } @@ -230,13 +230,13 @@ private synchronized void propagateErrors(FirebaseRemoteConfigException exceptio // Used for Tests only. @SuppressLint("VisibleForTests") public int getNumberOfFailedStreams() { - return metadataClient.getRealtimeBackoffMetadata().getNumFailedStreams(); + return sharedPrefsClient.getRealtimeBackoffMetadata().getNumFailedStreams(); } // Used for Tests only. @SuppressLint("VisibleForTests") public Date getBackoffEndTime() { - return metadataClient.getRealtimeBackoffMetadata().getBackoffEndTime(); + return sharedPrefsClient.getRealtimeBackoffMetadata().getBackoffEndTime(); } // TODO(issues/265): Make this an atomic operation within the Metadata class to avoid possible @@ -248,7 +248,7 @@ public Date getBackoffEndTime() { */ private void updateBackoffMetadataWithLastFailedStreamConnectionTime( Date lastFailedRealtimeStreamTime) { - int numFailedStreams = metadataClient.getRealtimeBackoffMetadata().getNumFailedStreams(); + int numFailedStreams = sharedPrefsClient.getRealtimeBackoffMetadata().getNumFailedStreams(); numFailedStreams++; @@ -256,7 +256,7 @@ private void updateBackoffMetadataWithLastFailedStreamConnectionTime( Date backoffEndTime = new Date(lastFailedRealtimeStreamTime.getTime() + backoffDurationInMillis); - metadataClient.setRealtimeBackoffMetadata(numFailedStreams, backoffEndTime); + sharedPrefsClient.setRealtimeBackoffMetadata(numFailedStreams, backoffEndTime); } /** @@ -362,7 +362,7 @@ public synchronized void retryHttpConnectionWhenBackoffEnds() { long retrySeconds = Math.max( 0, - metadataClient.getRealtimeBackoffMetadata().getBackoffEndTime().getTime() + sharedPrefsClient.getRealtimeBackoffMetadata().getBackoffEndTime().getTime() - currentTime.getTime()); makeRealtimeHttpConnection(retrySeconds); } @@ -473,8 +473,8 @@ public void beginRealtimeHttpStream() { return; } - ConfigMetadataClient.RealtimeBackoffMetadata backoffMetadata = - metadataClient.getRealtimeBackoffMetadata(); + ConfigSharedPrefsClient.RealtimeBackoffMetadata backoffMetadata = + sharedPrefsClient.getRealtimeBackoffMetadata(); Date currentTime = new Date(clock.currentTimeMillis()); if (currentTime.before(backoffMetadata.getBackoffEndTime())) { retryHttpConnectionWhenBackoffEnds(); @@ -506,7 +506,7 @@ public void beginRealtimeHttpStream() { if (responseCode == HttpURLConnection.HTTP_OK) { // Reset the retries remaining if we opened the connection without an exception. resetRetryCount(); - metadataClient.resetRealtimeBackoff(); + sharedPrefsClient.resetRealtimeBackoff(); // Start listening for realtime notifications. ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection); diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java similarity index 65% rename from firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClient.java rename to firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java index dddcb24bdb8..7ce24bc44f6 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java @@ -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; @@ -31,14 +34,20 @@ 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 java.util.Objects; +import org.json.JSONException; +import org.json.JSONObject; /** - * Client for handling Firebase Remote Config (FRC) metadata that is saved to disk and persisted - * across App life cycles. + * Client for handling Firebase Remote Config (FRC) metadata and custom signals that are saved to + * disk and persisted across App life cycles. * * @author Miraziz Yusupov */ -public class ConfigMetadataClient { +public class ConfigSharedPrefsClient { @Retention(SOURCE) @IntDef({ LAST_FETCH_STATUS_SUCCESS, @@ -75,65 +84,75 @@ public class ConfigMetadataClient { private static final String REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY = "realtime_backoff_end_time_in_millis"; - private final SharedPreferences frcMetadata; + /** 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 frcSharedPrefs; private final Object frcInfoLock; private final Object backoffMetadataLock; private final Object realtimeBackoffMetadataLock; + private final Object customSignalsLock; - public ConfigMetadataClient(SharedPreferences frcMetadata) { - this.frcMetadata = frcMetadata; + public ConfigSharedPrefsClient(SharedPreferences frcSharedPrefs) { + this.frcSharedPrefs = frcSharedPrefs; this.frcInfoLock = new Object(); this.backoffMetadataLock = new Object(); this.realtimeBackoffMetadataLock = new Object(); + this.customSignalsLock = new Object(); } public long getFetchTimeoutInSeconds() { - return frcMetadata.getLong(FETCH_TIMEOUT_IN_SECONDS_KEY, CONNECTION_TIMEOUT_IN_SECONDS); + return frcSharedPrefs.getLong(FETCH_TIMEOUT_IN_SECONDS_KEY, CONNECTION_TIMEOUT_IN_SECONDS); } public long getMinimumFetchIntervalInSeconds() { - return frcMetadata.getLong( + return frcSharedPrefs.getLong( MINIMUM_FETCH_INTERVAL_IN_SECONDS_KEY, DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS); } @LastFetchStatus int getLastFetchStatus() { - return frcMetadata.getInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_NO_FETCH_YET); + return frcSharedPrefs.getInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_NO_FETCH_YET); } Date getLastSuccessfulFetchTime() { return new Date( - frcMetadata.getLong( + frcSharedPrefs.getLong( LAST_SUCCESSFUL_FETCH_TIME_IN_MILLIS_KEY, LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET)); } @Nullable String getLastFetchETag() { - return frcMetadata.getString(LAST_FETCH_ETAG_KEY, null); + return frcSharedPrefs.getString(LAST_FETCH_ETAG_KEY, null); } long getLastTemplateVersion() { - return frcMetadata.getLong(LAST_TEMPLATE_VERSION, 0); + return frcSharedPrefs.getLong(LAST_TEMPLATE_VERSION, 0); } public FirebaseRemoteConfigInfo getInfo() { // A lock is used here to prevent the setters in this class from changing the state of - // frcMetadata during a getInfo call. + // frcSharedPrefs during a getInfo call. synchronized (frcInfoLock) { long lastSuccessfulFetchTimeInMillis = - frcMetadata.getLong( + frcSharedPrefs.getLong( LAST_SUCCESSFUL_FETCH_TIME_IN_MILLIS_KEY, LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET); @LastFetchStatus int lastFetchStatus = - frcMetadata.getInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_NO_FETCH_YET); + frcSharedPrefs.getInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_NO_FETCH_YET); FirebaseRemoteConfigSettings settings = new FirebaseRemoteConfigSettings.Builder() .setFetchTimeoutInSeconds( - frcMetadata.getLong(FETCH_TIMEOUT_IN_SECONDS_KEY, CONNECTION_TIMEOUT_IN_SECONDS)) + frcSharedPrefs.getLong( + FETCH_TIMEOUT_IN_SECONDS_KEY, CONNECTION_TIMEOUT_IN_SECONDS)) .setMinimumFetchIntervalInSeconds( - frcMetadata.getLong( + frcSharedPrefs.getLong( MINIMUM_FETCH_INTERVAL_IN_SECONDS_KEY, DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS)) .build(); @@ -147,14 +166,14 @@ public FirebaseRemoteConfigInfo getInfo() { } /** - * Clears all metadata values from memory and disk. + * Clears all metadata and custom signals values from memory and disk. * *

The method is blocking and returns only when the values in disk are also cleared. */ @WorkerThread public void clear() { synchronized (frcInfoLock) { - frcMetadata.edit().clear().commit(); + frcSharedPrefs.edit().clear().commit(); } } @@ -167,7 +186,7 @@ public void clear() { @WorkerThread public void setConfigSettings(FirebaseRemoteConfigSettings settings) { synchronized (frcInfoLock) { - frcMetadata + frcSharedPrefs .edit() .putLong(FETCH_TIMEOUT_IN_SECONDS_KEY, settings.getFetchTimeoutInSeconds()) .putLong( @@ -184,7 +203,7 @@ public void setConfigSettings(FirebaseRemoteConfigSettings settings) { */ public void setConfigSettingsWithoutWaitingOnDiskWrite(FirebaseRemoteConfigSettings settings) { synchronized (frcInfoLock) { - frcMetadata + frcSharedPrefs .edit() .putLong(FETCH_TIMEOUT_IN_SECONDS_KEY, settings.getFetchTimeoutInSeconds()) .putLong( @@ -195,7 +214,7 @@ public void setConfigSettingsWithoutWaitingOnDiskWrite(FirebaseRemoteConfigSetti void updateLastFetchAsSuccessfulAt(Date fetchTime) { synchronized (frcInfoLock) { - frcMetadata + frcSharedPrefs .edit() .putInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_SUCCESS) .putLong(LAST_SUCCESSFUL_FETCH_TIME_IN_MILLIS_KEY, fetchTime.getTime()) @@ -205,25 +224,25 @@ void updateLastFetchAsSuccessfulAt(Date fetchTime) { void updateLastFetchAsFailed() { synchronized (frcInfoLock) { - frcMetadata.edit().putInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_FAILURE).apply(); + frcSharedPrefs.edit().putInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_FAILURE).apply(); } } void updateLastFetchAsThrottled() { synchronized (frcInfoLock) { - frcMetadata.edit().putInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_THROTTLED).apply(); + frcSharedPrefs.edit().putInt(LAST_FETCH_STATUS_KEY, LAST_FETCH_STATUS_THROTTLED).apply(); } } void setLastFetchETag(String eTag) { synchronized (frcInfoLock) { - frcMetadata.edit().putString(LAST_FETCH_ETAG_KEY, eTag).apply(); + frcSharedPrefs.edit().putString(LAST_FETCH_ETAG_KEY, eTag).apply(); } } void setLastTemplateVersion(long templateVersion) { synchronized (frcInfoLock) { - frcMetadata.edit().putLong(LAST_TEMPLATE_VERSION, templateVersion).apply(); + frcSharedPrefs.edit().putLong(LAST_TEMPLATE_VERSION, templateVersion).apply(); } } @@ -234,14 +253,15 @@ void setLastTemplateVersion(long templateVersion) { BackoffMetadata getBackoffMetadata() { synchronized (backoffMetadataLock) { return new BackoffMetadata( - frcMetadata.getInt(NUM_FAILED_FETCHES_KEY, NO_FAILED_FETCHES), - new Date(frcMetadata.getLong(BACKOFF_END_TIME_IN_MILLIS_KEY, NO_BACKOFF_TIME_IN_MILLIS))); + frcSharedPrefs.getInt(NUM_FAILED_FETCHES_KEY, NO_FAILED_FETCHES), + new Date( + frcSharedPrefs.getLong(BACKOFF_END_TIME_IN_MILLIS_KEY, NO_BACKOFF_TIME_IN_MILLIS))); } } void setBackoffMetadata(int numFailedFetches, Date backoffEndTime) { synchronized (backoffMetadataLock) { - frcMetadata + frcSharedPrefs .edit() .putInt(NUM_FAILED_FETCHES_KEY, numFailedFetches) .putLong(BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime()) @@ -249,6 +269,77 @@ void setBackoffMetadata(int numFailedFetches, Date backoffEndTime) { } } + public void setCustomSignals(Map newCustomSignals) { + synchronized (customSignalsLock) { + // Retrieve existing custom signals + Map existingCustomSignals = getCustomSignals(); + // Tracks whether the custom signals have been modified. + boolean modified = false; + + for (Map.Entry 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) { + modified |= !Objects.equals(existingCustomSignals.put(key, value), value); + } else { + modified |= existingCustomSignals.remove(key) != null; + } + } + + // Check if the map has actually changed and the size limit + if (!modified) { + 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; + } + + frcSharedPrefs + .edit() + .putString(CUSTOM_SIGNALS, new JSONObject(existingCustomSignals).toString()) + .commit(); + + // Log the keys of the updated custom signals. + Log.d(TAG, "Keys of updated custom signals: " + getCustomSignals().keySet()); + } + } + + public Map getCustomSignals() { + String jsonString = frcSharedPrefs.getString(CUSTOM_SIGNALS, "{}"); + try { + JSONObject existingCustomSignalsJson = new JSONObject(jsonString); + Map custom_signals = new HashMap<>(); + Iterator 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); } @@ -286,16 +377,16 @@ Date getBackoffEndTime() { public RealtimeBackoffMetadata getRealtimeBackoffMetadata() { synchronized (realtimeBackoffMetadataLock) { return new RealtimeBackoffMetadata( - frcMetadata.getInt(NUM_FAILED_REALTIME_STREAMS_KEY, NO_FAILED_REALTIME_STREAMS), + frcSharedPrefs.getInt(NUM_FAILED_REALTIME_STREAMS_KEY, NO_FAILED_REALTIME_STREAMS), new Date( - frcMetadata.getLong( + frcSharedPrefs.getLong( REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, NO_BACKOFF_TIME_IN_MILLIS))); } } void setRealtimeBackoffMetadata(int numFailedStreams, Date backoffEndTime) { synchronized (realtimeBackoffMetadataLock) { - frcMetadata + frcSharedPrefs .edit() .putInt(NUM_FAILED_REALTIME_STREAMS_KEY, numFailedStreams) .putLong(REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime()) diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/FirebaseRemoteConfigInfoImpl.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/FirebaseRemoteConfigInfoImpl.java index 15f0bb00028..4a16f7bf4e9 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/FirebaseRemoteConfigInfoImpl.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/FirebaseRemoteConfigInfoImpl.java @@ -16,7 +16,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigInfo; import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LastFetchStatus; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LastFetchStatus; /** * Impl class for FirebaseRemoteConfigInfo. diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/CustomSignalsTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/CustomSignalsTest.java new file mode 100644 index 00000000000..cf97e17226e --- /dev/null +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/CustomSignalsTest.java @@ -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 static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +/** Unit tests for the {@link CustomSignals}.*/ +public class CustomSignalsTest { + @Test + public void testCustomSignals_builderPutString() { + CustomSignals customSignals = + new CustomSignals.Builder().put("key1", "value1").put("key2", "value2").build(); + Map expectedSignals = ImmutableMap.of("key1", "value1", "key2", "value2"); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } + + @Test + public void testCustomSignals_builderPutLong() { + CustomSignals customSignals = + new CustomSignals.Builder().put("key1", 123L).put("key2", 456L).build(); + Map expectedSignals = ImmutableMap.of("key1", "123", "key2", "456"); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } + + @Test + public void testCustomSignals_builderPutDouble() { + CustomSignals customSignals = + new CustomSignals.Builder().put("key1", 12.34).put("key2", 56.78).build(); + Map expectedSignals = ImmutableMap.of("key1", "12.34", "key2", "56.78"); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } + + @Test + public void testCustomSignals_builderPutNullValue() { + CustomSignals customSignals = new CustomSignals.Builder().put("key1", null).build(); + Map expectedSignals = new HashMap<>(); + expectedSignals.put("key1", null); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } + + @Test + public void testCustomSignals_builderPutDuplicateKeys() { + CustomSignals customSignals = + new CustomSignals.Builder() + .put("key1", "value1") + .put("key1", "value2") + .put("key1", "value3") + .build(); + Map expectedSignals = ImmutableMap.of("key1", "value3"); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } + + @Test + public void testCustomSignals_builderPutMixedTypes() { + CustomSignals customSignals = + new CustomSignals.Builder() + .put("key1", "value1") + .put("key2", 123L) + .put("key3", 45.67) + .put("key4", null) + .build(); + Map expectedSignals = new HashMap<>(); + expectedSignals.put("key1", "value1"); + expectedSignals.put("key2", "123"); + expectedSignals.put("key3", "45.67"); + expectedSignals.put("key4", null); + assertThat(customSignals.customSignals).isEqualTo(expectedSignals); + } +} diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 81f292a4c2b..fffc439dc2b 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -72,9 +72,9 @@ 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.ConfigRealtimeHttpClient; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient; import com.google.firebase.remoteconfig.internal.FakeHttpURLConnection; import com.google.firebase.remoteconfig.internal.Personalization; import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler; @@ -155,7 +155,7 @@ public final class FirebaseRemoteConfigTest { @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 ConfigAutoFetch mockConfigAutoFetch; @@ -192,7 +192,7 @@ public final class FirebaseRemoteConfigTest { private ConfigContainer realtimeFetchedContainer; private ConfigAutoFetch configAutoFetch; private ConfigRealtimeHttpClient configRealtimeHttpClient; - private ConfigMetadataClient realtimeMetadataClient; + private ConfigSharedPrefsClient realtimeSharedPrefsClient; private FetchResponse firstFetchedContainerResponse; @@ -240,7 +240,7 @@ public void setUp() throws Exception { mockDefaultsCache, mockFetchHandler, mockGetHandler, - metadataClient, + sharedPrefsClient, mockConfigRealtimeHandler, mockRolloutsStateSubscriptionsHandler); @@ -259,7 +259,7 @@ public void setUp() throws Exception { mockFireperfDefaultsCache, mockFireperfFetchHandler, mockFireperfGetHandler, - RemoteConfigComponent.getMetadataClient(context, APP_ID, FIREPERF_NAMESPACE), + RemoteConfigComponent.getSharedPrefsClient(context, APP_ID, FIREPERF_NAMESPACE), mockRolloutsStateSubscriptionsHandler); personalizationFrc = @@ -276,7 +276,8 @@ public void setUp() throws Exception { mockDefaultsCache, mockFetchHandler, parameterHandler, - RemoteConfigComponent.getMetadataClient(context, APP_ID, PERSONALIZATION_NAMESPACE), + RemoteConfigComponent.getSharedPrefsClient( + context, APP_ID, PERSONALIZATION_NAMESPACE), mockRolloutsStateSubscriptionsHandler); firstFetchedContainer = @@ -349,8 +350,9 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { listeners, mockRetryListener, scheduledExecutorService); - realtimeMetadataClient = - new ConfigMetadataClient(context.getSharedPreferences("test_file", Context.MODE_PRIVATE)); + realtimeSharedPrefsClient = + new ConfigSharedPrefsClient( + context.getSharedPreferences("test_file", Context.MODE_PRIVATE)); configRealtimeHttpClient = new ConfigRealtimeHttpClient( firebaseApp, @@ -360,7 +362,7 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { context, "firebase", listeners, - realtimeMetadataClient, + realtimeSharedPrefsClient, scheduledExecutorService); } @@ -1024,7 +1026,7 @@ public void getLong_fireperfNamespace_keyExists_returnsRemoteValue() { @Test public void getInfo_returnsInfo() { - when(metadataClient.getInfo()).thenReturn(mockFrcInfo); + when(sharedPrefsClient.getInfo()).thenReturn(mockFrcInfo); long fetchTimeInMillis = 100L; int lastFetchStatus = LAST_FETCH_STATUS_THROTTLED; @@ -1071,11 +1073,11 @@ public void clear_hasSettings_clearsEverything() { verify(mockActivatedCache).clear(); verify(mockFetchedCache).clear(); verify(mockDefaultsCache).clear(); - verify(metadataClient).clear(); + verify(sharedPrefsClient).clear(); } @Test - public void setConfigSettingsAsync_updatesMetadata() { + public void setConfigSettingsAsync_updatesSharedPrefs() { long fetchTimeout = 13L; long minimumFetchInterval = 666L; FirebaseRemoteConfigSettings frcSettings = @@ -1087,7 +1089,7 @@ public void setConfigSettingsAsync_updatesMetadata() { Task setterTask = frc.setConfigSettingsAsync(frcSettings); assertThat(setterTask.isSuccessful()).isTrue(); - verify(metadataClient).setConfigSettings(frcSettings); + verify(sharedPrefsClient).setConfigSettings(frcSettings); } @Test @@ -1658,6 +1660,22 @@ public void realtimeRequest_setRequestParams_succeedsWithCorrectParams() throws assertThat(fakeConnection.getRequestMethod()).isEqualTo("POST"); } + @Test + public void setCustomSignals_succeeds_and_calls_sharedPrefsClient() { + CustomSignals customSignals = + new CustomSignals.Builder() + .put("key1", "value1") + .put("key2", 123L) + .put("key3", 12.34) + .put("key4", null) + .build(); + + Task setterTask = frc.setCustomSignals(customSignals); + + assertThat(setterTask.isSuccessful()).isTrue(); + verify(sharedPrefsClient).setCustomSignals(customSignals.customSignals); + } + private static void loadCacheWithConfig( ConfigCacheClient cacheClient, ConfigContainer container) { when(cacheClient.getBlocking()).thenReturn(container); diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java index c6869267a4d..e0ee9220855 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java @@ -39,7 +39,7 @@ import com.google.firebase.remoteconfig.internal.ConfigFetchHandler; import com.google.firebase.remoteconfig.internal.ConfigFetchHttpClient; import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient; import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler; import com.google.firebase.remoteconfig.interop.rollouts.RolloutsStateSubscriber; import java.util.Date; @@ -73,7 +73,7 @@ public class RemoteConfigComponentTest { @Mock private ConfigCacheClient mockDefaultsCache; @Mock private ConfigFetchHandler mockFetchHandler; @Mock private ConfigGetParameterHandler mockGetParameterHandler; - @Mock private ConfigMetadataClient mockMetadataClient; + @Mock private ConfigSharedPrefsClient mockSharedPrefsClient; @Mock private RolloutsStateSubscriptionsHandler mockRolloutsStateSubscriptionsHandler; @Mock private RolloutsStateSubscriber mockRolloutsStateSubscriber; @@ -82,7 +82,7 @@ public class RemoteConfigComponentTest { private ExecutorService directExecutor; private ScheduledExecutorService scheduledExecutorService; private FirebaseApp defaultApp; - private ConfigMetadataClient metadataClient; + private ConfigSharedPrefsClient sharedPrefsClient; @Before public void setUp() { @@ -94,7 +94,8 @@ public void setUp() { defaultApp = initializeFirebaseApp(context); - metadataClient = RemoteConfigComponent.getMetadataClient(context, APP_ID, "personalization"); + sharedPrefsClient = + RemoteConfigComponent.getSharedPrefsClient(context, APP_ID, "personalization"); when(mockFirebaseApp.getOptions()) .thenReturn(new FirebaseOptions.Builder().setApplicationId(APP_ID).build()); @@ -106,7 +107,7 @@ public void setUp() { public void frc2p_doesNotCallAbt() throws Exception { FirebaseRemoteConfig fireperfFrc = - getFrcInstanceFromComponentWithMetadataClient( + getFrcInstanceFromComponentWithSharedPrefsClient( getNewFrcComponent(), /* namespace= */ "fireperf"); loadConfigsWithExperimentsForActivate(); @@ -123,7 +124,7 @@ public void frcNonMainFirebaseApp_doesNotCallAbt() throws Exception { when(mockFirebaseApp.getName()).thenReturn("secondary"); FirebaseRemoteConfig frc = - getFrcInstanceFromComponentWithMetadataClient( + getFrcInstanceFromComponentWithSharedPrefsClient( getNewFrcComponentWithoutLoadingDefault(), DEFAULT_NAMESPACE); loadConfigsWithExperimentsForActivate(); @@ -139,7 +140,7 @@ public void getFetchHandler_nonMainFirebaseApp_doesNotUseAnalytics() { ConfigFetchHandler fetchHandler = getNewFrcComponent() - .getFetchHandler(DEFAULT_NAMESPACE, mockFetchedCache, mockMetadataClient); + .getFetchHandler(DEFAULT_NAMESPACE, mockFetchedCache, mockSharedPrefsClient); assertThat(fetchHandler.getAnalyticsConnector().get()).isNull(); } @@ -149,10 +150,12 @@ public void getFetchHandler_nonMainFirebaseApp_doesNotUseAnalytics() { getFrcBackendApiClient_fetchTimeoutIsNotSet_buildsConfigFetchHttpClientWithDefaultConnectionTimeout() { RemoteConfigComponent frcComponent = defaultApp.get(RemoteConfigComponent.class); - when(mockMetadataClient.getFetchTimeoutInSeconds()).thenReturn(CONNECTION_TIMEOUT_IN_SECONDS); + when(mockSharedPrefsClient.getFetchTimeoutInSeconds()) + .thenReturn(CONNECTION_TIMEOUT_IN_SECONDS); ConfigFetchHttpClient frcBackendClient = - frcComponent.getFrcBackendApiClient(DUMMY_API_KEY, DEFAULT_NAMESPACE, mockMetadataClient); + frcComponent.getFrcBackendApiClient( + DUMMY_API_KEY, DEFAULT_NAMESPACE, mockSharedPrefsClient); int actualConnectTimeout = getConnectTimeoutInSeconds(frcBackendClient); int actualReadTimeout = getReadTimeoutInSeconds(frcBackendClient); @@ -167,11 +170,12 @@ public void getFetchHandler_nonMainFirebaseApp_doesNotUseAnalytics() { RemoteConfigComponent frcComponent = defaultApp.get(RemoteConfigComponent.class); long customConnectionTimeoutInSeconds = 2 * CONNECTION_TIMEOUT_IN_SECONDS; - when(mockMetadataClient.getFetchTimeoutInSeconds()) + when(mockSharedPrefsClient.getFetchTimeoutInSeconds()) .thenReturn(customConnectionTimeoutInSeconds); ConfigFetchHttpClient frcBackendClient = - frcComponent.getFrcBackendApiClient(DUMMY_API_KEY, DEFAULT_NAMESPACE, mockMetadataClient); + frcComponent.getFrcBackendApiClient( + DUMMY_API_KEY, DEFAULT_NAMESPACE, mockSharedPrefsClient); int actualConnectTimeout = getConnectTimeoutInSeconds(frcBackendClient); int actualReadTimeout = getReadTimeoutInSeconds(frcBackendClient); @@ -181,9 +185,9 @@ public void getFetchHandler_nonMainFirebaseApp_doesNotUseAnalytics() { @Test public void registerRolloutsStateSubscriber_firebaseNamespace_callsSubscriptionHandler() { - // Mock metadata client response since Realtime handler can't be mocked here. - when(mockMetadataClient.getRealtimeBackoffMetadata()) - .thenReturn(new ConfigMetadataClient.RealtimeBackoffMetadata(0, new Date())); + // Mock shared preference client response since Realtime handler can't be mocked here. + when(mockSharedPrefsClient.getRealtimeBackoffMetadata()) + .thenReturn(new ConfigSharedPrefsClient.RealtimeBackoffMetadata(0, new Date())); RemoteConfigComponent frcComponent = getNewFrcComponentWithoutLoadingDefault(); FirebaseRemoteConfig instance = getFrcInstanceFromComponent(frcComponent, DEFAULT_NAMESPACE); @@ -216,7 +220,7 @@ private RemoteConfigComponent getNewFrcComponentWithoutLoadingDefault() { /* loadGetDefault= */ false); } - private FirebaseRemoteConfig getFrcInstanceFromComponentWithMetadataClient( + private FirebaseRemoteConfig getFrcInstanceFromComponentWithSharedPrefsClient( RemoteConfigComponent frcComponent, String namespace) { return frcComponent.get( mockFirebaseApp, @@ -229,7 +233,7 @@ private FirebaseRemoteConfig getFrcInstanceFromComponentWithMetadataClient( mockDefaultsCache, mockFetchHandler, mockGetParameterHandler, - metadataClient, + sharedPrefsClient, mockRolloutsStateSubscriptionsHandler); } @@ -246,7 +250,7 @@ private FirebaseRemoteConfig getFrcInstanceFromComponent( mockDefaultsCache, mockFetchHandler, mockGetParameterHandler, - mockMetadataClient, + mockSharedPrefsClient, mockRolloutsStateSubscriptionsHandler); } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigTests.kt b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigTests.kt index 7624ee3827d..81cb8115661 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigTests.kt +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigTests.kt @@ -29,8 +29,8 @@ import com.google.firebase.platforminfo.UserAgentPublisher 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 @@ -139,7 +139,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) ) @@ -147,6 +147,19 @@ class ConfigTests : BaseTestCase() { `when`(mockGetHandler.getValue("KEY")).thenReturn(StringRemoteConfigValue("non default value")) assertThat(remoteConfig["KEY"].asString()).isEqualTo("non default value") } + + @Test + fun `Custom Signals builder support multiple types`() { + val customSignals = customSignals { + put("key1", "value1") + put("key2", 123L) + put("key3", 45.67) + put("key4", null) + } + val expectedSignals = + mapOf("key1" to "value1", "key2" to "123", "key3" to "45.67", "key4" to null) + assertThat(customSignals.customSignals).isEqualTo(expectedSignals) + } } @RunWith(RobolectricTestRunner::class) diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/TestConstructorUtil.kt b/firebase-config/src/test/java/com/google/firebase/remoteconfig/TestConstructorUtil.kt index e6167b5ab09..4c153bff1e3 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/TestConstructorUtil.kt +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/TestConstructorUtil.kt @@ -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 @@ -41,7 +41,7 @@ fun createRemoteConfig( defaultConfigsCache: ConfigCacheClient, fetchHandler: ConfigFetchHandler, getHandler: ConfigGetParameterHandler, - frcMetadata: ConfigMetadataClient, + frcSharedPrefs: ConfigSharedPrefsClient, realtimeHandler: ConfigRealtimeHandler, rolloutsStateSubscriptionsHandler: RolloutsStateSubscriptionsHandler ): FirebaseRemoteConfig { @@ -56,7 +56,7 @@ fun createRemoteConfig( defaultConfigsCache, fetchHandler, getHandler, - frcMetadata, + frcSharedPrefs, realtimeHandler, rolloutsStateSubscriptionsHandler ) diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandlerTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandlerTest.java index ed2781710fd..740a00191ec 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandlerTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandlerTest.java @@ -29,9 +29,9 @@ import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS; import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FIRST_OPEN_TIME_KEY; import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.HTTP_TOO_MANY_REQUESTS; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LAST_FETCH_TIME_NO_FETCH_YET; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.NO_BACKOFF_TIME; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.NO_FAILED_FETCHES; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LAST_FETCH_TIME_NO_FETCH_YET; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.NO_BACKOFF_TIME; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.NO_FAILED_FETCHES; import static com.google.firebase.remoteconfig.testutil.Assert.assertThrows; import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; @@ -70,7 +70,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigServerException; import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; import com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FetchResponse; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient.BackoffMetadata; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.BackoffMetadata; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; @@ -127,7 +127,7 @@ public class ConfigFetchHandlerTest { private Context context; @Mock private FirebaseInstallationsApi mockFirebaseInstallations; - private ConfigMetadataClient metadataClient; + private ConfigSharedPrefsClient sharedPrefsClient; private ConfigFetchHandler fetchHandler; @@ -142,8 +142,9 @@ public void setUp() throws Exception { directExecutor = MoreExecutors.directExecutor(); context = ApplicationProvider.getApplicationContext(); mockClock = new MockClock(0L); - metadataClient = - new ConfigMetadataClient(context.getSharedPreferences("test_file", Context.MODE_PRIVATE)); + sharedPrefsClient = + new ConfigSharedPrefsClient( + context.getSharedPreferences("test_file", Context.MODE_PRIVATE)); loadBackendApiClient(); loadInstallationIdAndAuthToken(); @@ -201,7 +202,8 @@ public void fetch_firstFetch_includesInstallationAuthToken() throws Exception { /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } @Test @@ -400,7 +402,8 @@ public void fetch_gettingFetchCacheFails_doesNotThrowException() throws Exceptio @Test public void fetch_fetchBackendCallFails_taskThrowsException() throws Exception { - when(mockBackendFetchApiClient.fetch(any(), any(), any(), any(), any(), any(), any(), any())) + when(mockBackendFetchApiClient.fetch( + any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenThrow( new FirebaseRemoteConfigClientException("Fetch failed due to an unexpected error.")); @@ -549,7 +552,7 @@ public void fetch_getsMultipleFailedResponsesFromServer_resetsBackoffAfterSucces assertWithMessage("Fetch() failed!").that(fetchTask.isSuccessful()).isTrue(); - BackoffMetadata backoffMetadata = metadataClient.getBackoffMetadata(); + BackoffMetadata backoffMetadata = sharedPrefsClient.getBackoffMetadata(); assertThat(backoffMetadata.getNumFailedFetches()).isEqualTo(NO_FAILED_FETCHES); assertThat(backoffMetadata.getBackoffEndTime()).isEqualTo(NO_BACKOFF_TIME); } @@ -706,8 +709,9 @@ public void fetch_firstAndOnlyFetchFails_metadataFailStatusAndNoFetchYetTime() t fetchHandler.fetch(); - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); - assertThat(metadataClient.getLastSuccessfulFetchTime()).isEqualTo(LAST_FETCH_TIME_NO_FETCH_YET); + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) + .isEqualTo(LAST_FETCH_TIME_NO_FETCH_YET); } @Test @@ -716,8 +720,8 @@ public void fetch_fetchSucceeds_metadataSuccessStatusAndFetchTimeUpdated() throw fetchHandler.fetch(); - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); - assertThat(metadataClient.getLastSuccessfulFetchTime()) + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) .isEqualTo(firstFetchedContainer.getFetchTime()); } @@ -731,8 +735,8 @@ public void fetch_firstFetchSucceedsSecondFetchFails_failStatusAndFirstFetchTime fetchHandler.fetch(/* minimumFetchIntervalInSeconds= */ 0); - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); - assertThat(metadataClient.getLastSuccessfulFetchTime()) + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) .isEqualTo(firstFetchedContainer.getFetchTime()); } @@ -745,8 +749,8 @@ public void getInfo_twoFetchesSucceed_successStatusAndSecondFetchTime() throws E fetchHandler.fetch(/* minimumFetchIntervalInSeconds= */ 0); - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); - assertThat(metadataClient.getLastSuccessfulFetchTime()) + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) .isEqualTo(secondFetchedContainer.getFetchTime()); } @@ -759,11 +763,35 @@ public void getInfo_hitsThrottleLimit_throttledStatus() throws Exception { fetchHandler.fetch(/* minimumFetchIntervalInSeconds= */ 0); - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_THROTTLED); - assertThat(metadataClient.getLastSuccessfulFetchTime()) + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_THROTTLED); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) .isEqualTo(firstFetchedContainer.getFetchTime()); } + @Test + public void fetch_usesLatestCustomSignals() throws Exception { + Map customSignals = + ImmutableMap.of( + "subscription", "premium", + "age", "20"); + sharedPrefsClient.setCustomSignals(customSignals); + fetchCallToHttpClientUpdatesClockAndReturnsConfig(firstFetchedContainer); + fetchHandler.fetch(); + + verify(mockBackendFetchApiClient) + .fetch( + any(HttpURLConnection.class), + /* instanceId= */ any(), + /* instanceIdToken= */ any(), + /* analyticsUserProperties= */ any(), + /* lastFetchETag= */ any(), + /* customHeaders= */ any(), + /* firstOpenTime= */ any(), + /* currentTime= */ any(), + /* customSignals= */ eq(sharedPrefsClient.getCustomSignals())); + assertThat(sharedPrefsClient.getCustomSignals()).isEqualTo(customSignals); + } + private ConfigFetchHandler getNewFetchHandler(AnalyticsConnector analyticsConnector) { ConfigFetchHandler fetchHandler = spy( @@ -775,7 +803,7 @@ private ConfigFetchHandler getNewFetchHandler(AnalyticsConnector analyticsConnec mockRandom, mockFetchedCache, mockBackendFetchApiClient, - metadataClient, + sharedPrefsClient, /* customHttpHeaders= */ ImmutableMap.of())); return fetchHandler; } @@ -809,7 +837,8 @@ private void setBackendResponseConfigsTo(ConfigContainer container) throws Excep /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } private void setBackendResponseToNoChange(Date date) throws Exception { @@ -821,7 +850,8 @@ private void setBackendResponseToNoChange(Date date) throws Exception { /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any())) + /* currentTime= */ any(), + /* customSignals= */ any())) .thenReturn(FetchResponse.forBackendHasNoUpdates(date, firstFetchedContainer)); } @@ -836,7 +866,8 @@ private void fetchCallToBackendThrowsException(int httpErrorCode) throws Excepti /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } /** @@ -875,7 +906,8 @@ private void callFetchAssertThrottledAndAdvanceClock(int httpCode) throws Except long backoffDurationInMillis = loadAndGetNextBackoffDuration( - /* numFailedFetches= */ metadataClient.getBackoffMetadata().getNumFailedFetches() + 1); + /* numFailedFetches= */ sharedPrefsClient.getBackoffMetadata().getNumFailedFetches() + + 1); assertThrowsThrottledException(fetchHandler.fetch(/* minimumFetchIntervalInSeconds= */ 0L)); @@ -900,7 +932,7 @@ private long loadAndGetNextBackoffDuration(int numFailedFetches) { } private void setMinimumFetchIntervalInMetadata(long minimumFetchIntervalInSeconds) { - metadataClient.setConfigSettings( + sharedPrefsClient.setConfigSettings( new FirebaseRemoteConfigSettings.Builder() .setMinimumFetchIntervalInSeconds(minimumFetchIntervalInSeconds) .build()); @@ -916,7 +948,8 @@ private void verifyBackendIsCalled() throws Exception { /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } private void verifyBackendIsCalled(Map userProperties, Long firstOpenTime) @@ -930,7 +963,8 @@ private void verifyBackendIsCalled(Map userProperties, Long firs /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ eq(firstOpenTime), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } private void verifyBackendIsNeverCalled() throws Exception { @@ -943,7 +977,8 @@ private void verifyBackendIsNeverCalled() throws Exception { /* lastFetchETag= */ any(), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); + /* currentTime= */ any(), + /* customSignals= */ any()); } private void verifyETags(@Nullable String requestETag, String responseETag) throws Exception { @@ -956,8 +991,9 @@ private void verifyETags(@Nullable String requestETag, String responseETag) thro /* lastFetchETag= */ eq(requestETag), /* customHeaders= */ any(), /* firstOpenTime= */ any(), - /* currentTime= */ any()); - assertThat(metadataClient.getLastFetchETag()).isEqualTo(responseETag); + /* currentTime= */ any(), + /* customSignals= */ any()); + assertThat(sharedPrefsClient.getLastFetchETag()).isEqualTo(responseETag); } private void loadBackendApiClient() throws Exception { @@ -966,7 +1002,7 @@ private void loadBackendApiClient() throws Exception { } private void loadETags(String requestETag, String responseETag) { - metadataClient.setLastFetchETag(requestETag); + sharedPrefsClient.setLastFetchETag(requestETag); this.responseETag = responseETag; } @@ -981,7 +1017,7 @@ private void loadCacheAndClockWithConfig( when(cacheClient.getBlocking()).thenReturn(container); when(cacheClient.get()).thenReturn(Tasks.forResult(container)); mockClock.setCurrentTime(container.getFetchTime().getTime()); - metadataClient.updateLastFetchAsSuccessfulAt(container.getFetchTime()); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(container.getFetchTime()); } private static void cachePutReturnsConfig( diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClientTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClientTest.java index edbded6f79b..ae068b4f8d2 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClientTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClientTest.java @@ -344,7 +344,8 @@ private FetchResponse fetch(String eTag) throws Exception { eTag, /* customHeaders= */ ImmutableMap.of(), /* firstOpenTime= */ null, - /* currentTime= */ new Date(mockClock.currentTimeMillis())); + /* currentTime= */ new Date(mockClock.currentTimeMillis()), + /* customSignals= */ ImmutableMap.of()); } private FetchResponse fetch(String eTag, Map userProperties, Long firstOpenTime) @@ -357,7 +358,8 @@ private FetchResponse fetch(String eTag, Map userProperties, Lon eTag, /* customHeaders= */ ImmutableMap.of(), firstOpenTime, - new Date(mockClock.currentTimeMillis())); + new Date(mockClock.currentTimeMillis()), + /* customSignals= */ ImmutableMap.of()); } private FetchResponse fetch(String eTag, Map customHeaders) throws Exception { @@ -369,7 +371,8 @@ private FetchResponse fetch(String eTag, Map customHeaders) thro eTag, customHeaders, /* firstOpenTime= */ null, - new Date(mockClock.currentTimeMillis())); + new Date(mockClock.currentTimeMillis()), + /* customSignals= */ ImmutableMap.of()); } private FetchResponse fetchWithoutInstallationId() throws Exception { @@ -381,7 +384,8 @@ private FetchResponse fetchWithoutInstallationId() throws Exception { /* lastFetchETag= */ "bogus-etag", /* customHeaders= */ ImmutableMap.of(), /* firstOpenTime= */ null, - new Date(mockClock.currentTimeMillis())); + new Date(mockClock.currentTimeMillis()), + /* customSignals= */ ImmutableMap.of()); } private FetchResponse fetchWithoutInstallationAuthToken() throws Exception { @@ -393,7 +397,8 @@ private FetchResponse fetchWithoutInstallationAuthToken() throws Exception { /* lastFetchETag= */ "bogus-etag", /* customHeaders= */ ImmutableMap.of(), /* firstOpenTime= */ null, - new Date(mockClock.currentTimeMillis())); + new Date(mockClock.currentTimeMillis()), + /* customSignals= */ ImmutableMap.of()); } private void setServerResponseTo(JSONObject requestBody, String eTag) { diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClientTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClientTest.java similarity index 60% rename from firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClientTest.java rename to firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClientTest.java index e2c38df4a30..2edfb171ddc 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigMetadataClientTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClientTest.java @@ -21,20 +21,24 @@ import static com.google.firebase.remoteconfig.FirebaseRemoteConfig.LAST_FETCH_STATUS_THROTTLED; import static com.google.firebase.remoteconfig.RemoteConfigComponent.CONNECTION_TIMEOUT_IN_SECONDS; import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LAST_FETCH_TIME_NO_FETCH_YET; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.NO_BACKOFF_TIME; -import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.NO_FAILED_FETCHES; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LAST_FETCH_TIME_NO_FETCH_YET; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.NO_BACKOFF_TIME; +import static com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.NO_FAILED_FETCHES; import android.content.Context; import android.content.SharedPreferences; import androidx.test.core.app.ApplicationProvider; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; import com.google.firebase.remoteconfig.FirebaseRemoteConfigInfo; import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient.BackoffMetadata; -import com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LastFetchStatus; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.BackoffMetadata; +import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient.LastFetchStatus; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,121 +46,123 @@ import org.robolectric.annotation.Config; /** - * Unit tests for the {@link ConfigMetadataClient}. + * Unit tests for the {@link ConfigSharedPrefsClient}. * * @author Miraziz Yusupov */ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class ConfigMetadataClientTest { - private ConfigMetadataClient metadataClient; +public class ConfigSharedPrefsClientTest { + private ConfigSharedPrefsClient sharedPrefsClient; private FirebaseRemoteConfigSettings.Builder settingsBuilder; @Before public void setUp() { - SharedPreferences metadata = + SharedPreferences sharedPrefs = ApplicationProvider.getApplicationContext() .getSharedPreferences("TEST_FILE_NAME", Context.MODE_PRIVATE); - metadata.edit().clear().commit(); + sharedPrefs.edit().clear().commit(); - metadataClient = new ConfigMetadataClient(metadata); + sharedPrefsClient = new ConfigSharedPrefsClient(sharedPrefs); settingsBuilder = new FirebaseRemoteConfigSettings.Builder(); } @Test public void getFetchTimeoutInSeconds_isNotSet_returnsDefault() { - assertThat(metadataClient.getFetchTimeoutInSeconds()).isEqualTo(CONNECTION_TIMEOUT_IN_SECONDS); + assertThat(sharedPrefsClient.getFetchTimeoutInSeconds()) + .isEqualTo(CONNECTION_TIMEOUT_IN_SECONDS); } @Test public void getFetchTimeoutInSeconds_isSetTo10Seconds_returns10Seconds() { long expectedFetchTimeout = 10L; - metadataClient.setConfigSettings( + sharedPrefsClient.setConfigSettings( settingsBuilder.setFetchTimeoutInSeconds(expectedFetchTimeout).build()); - long fetchTimeout = metadataClient.getFetchTimeoutInSeconds(); + long fetchTimeout = sharedPrefsClient.getFetchTimeoutInSeconds(); assertThat(fetchTimeout).isEqualTo(expectedFetchTimeout); } @Test public void getMinimumFetchIntervalInSeconds_isNotSet_returnsDefault() { - assertThat(metadataClient.getMinimumFetchIntervalInSeconds()) + assertThat(sharedPrefsClient.getMinimumFetchIntervalInSeconds()) .isEqualTo(DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS); } @Test public void getMinimumFetchIntervalInSeconds_isSetTo10Seconds_returns10Seconds() { long expectedMinimumFetchInterval = 10L; - metadataClient.setConfigSettings( + sharedPrefsClient.setConfigSettings( settingsBuilder.setMinimumFetchIntervalInSeconds(expectedMinimumFetchInterval).build()); - long minimumFetchInterval = metadataClient.getMinimumFetchIntervalInSeconds(); + long minimumFetchInterval = sharedPrefsClient.getMinimumFetchIntervalInSeconds(); assertThat(minimumFetchInterval).isEqualTo(expectedMinimumFetchInterval); } @Test public void getLastFetchStatus_isNotSet_returnsZero() { - assertThat(metadataClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_NO_FETCH_YET); + assertThat(sharedPrefsClient.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_NO_FETCH_YET); } @Test public void getLastFetchStatus_isSetToSuccess_returnsSuccess() { - metadataClient.updateLastFetchAsSuccessfulAt(new Date(100L)); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(new Date(100L)); - @LastFetchStatus int lastFetchStatus = metadataClient.getLastFetchStatus(); + @LastFetchStatus int lastFetchStatus = sharedPrefsClient.getLastFetchStatus(); assertThat(lastFetchStatus).isEqualTo(LAST_FETCH_STATUS_SUCCESS); } @Test public void getLastSuccessfulFetchTime_isNotSet_returnsZero() { - assertThat(metadataClient.getLastSuccessfulFetchTime()).isEqualTo(LAST_FETCH_TIME_NO_FETCH_YET); + assertThat(sharedPrefsClient.getLastSuccessfulFetchTime()) + .isEqualTo(LAST_FETCH_TIME_NO_FETCH_YET); } @Test public void getLastSuccessfulFetchTime_isSet_returnsTime() { Date fetchTime = new Date(1000L); - metadataClient.updateLastFetchAsSuccessfulAt(fetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(fetchTime); - Date lastSuccessfulFetchTime = metadataClient.getLastSuccessfulFetchTime(); + Date lastSuccessfulFetchTime = sharedPrefsClient.getLastSuccessfulFetchTime(); assertThat(lastSuccessfulFetchTime).isEqualTo(fetchTime); } @Test public void getLastFetchETag_isNotSet_returnsEmptyString() { - assertThat(metadataClient.getLastFetchETag()).isNull(); + assertThat(sharedPrefsClient.getLastFetchETag()).isNull(); } @Test public void getLastFetchETag_isSet_returnsETag() { String expectedETag = "an etag"; - metadataClient.setLastFetchETag(expectedETag); + sharedPrefsClient.setLastFetchETag(expectedETag); - String eTag = metadataClient.getLastFetchETag(); + String eTag = sharedPrefsClient.getLastFetchETag(); assertThat(eTag).isEqualTo(expectedETag); } @Test public void getLastTemplateVersion_isNotSet_returnsDefault() { - assertThat(metadataClient.getLastTemplateVersion()).isEqualTo(0); + assertThat(sharedPrefsClient.getLastTemplateVersion()).isEqualTo(0); } @Test public void getLastTemplateVersion_isSet_returnsTemplateVersion() { - metadataClient.setLastTemplateVersion(1); - assertThat(metadataClient.getLastTemplateVersion()).isEqualTo(1); + sharedPrefsClient.setLastTemplateVersion(1); + assertThat(sharedPrefsClient.getLastTemplateVersion()).isEqualTo(1); } @Test public void getRealtimeBackoffMetadata_isNotSet_returnsNoFailedStreamsAndNotThrottled() { - ConfigMetadataClient.RealtimeBackoffMetadata defaultRealtimeBackoffMetadata = - metadataClient.getRealtimeBackoffMetadata(); + ConfigSharedPrefsClient.RealtimeBackoffMetadata defaultRealtimeBackoffMetadata = + sharedPrefsClient.getRealtimeBackoffMetadata(); assertThat(defaultRealtimeBackoffMetadata.getNumFailedStreams()).isEqualTo(NO_FAILED_FETCHES); assertThat(defaultRealtimeBackoffMetadata.getBackoffEndTime()).isEqualTo(NO_BACKOFF_TIME); @@ -166,10 +172,10 @@ public void getRealtimeBackoffMetadata_isNotSet_returnsNoFailedStreamsAndNotThro public void getRealtimeBackoffMetadata_hasValues_returnsValues() { int numFailedStreams = 5; Date backoffEndTime = new Date(1000L); - metadataClient.setRealtimeBackoffMetadata(numFailedStreams, backoffEndTime); + sharedPrefsClient.setRealtimeBackoffMetadata(numFailedStreams, backoffEndTime); - ConfigMetadataClient.RealtimeBackoffMetadata backoffMetadata = - metadataClient.getRealtimeBackoffMetadata(); + ConfigSharedPrefsClient.RealtimeBackoffMetadata backoffMetadata = + sharedPrefsClient.getRealtimeBackoffMetadata(); assertThat(backoffMetadata.getNumFailedStreams()).isEqualTo(numFailedStreams); assertThat(backoffMetadata.getBackoffEndTime()).isEqualTo(backoffEndTime); @@ -177,26 +183,26 @@ public void getRealtimeBackoffMetadata_hasValues_returnsValues() { @Test public void resetRealtimeBackoff_hasValues_clearsAllValues() { - metadataClient.setRealtimeBackoffMetadata( + sharedPrefsClient.setRealtimeBackoffMetadata( /* numFailedStreams= */ 5, /* backoffEndTime= */ new Date(1000L)); - ConfigMetadataClient.RealtimeBackoffMetadata realtimeBackoffMetadata = - metadataClient.getRealtimeBackoffMetadata(); + ConfigSharedPrefsClient.RealtimeBackoffMetadata realtimeBackoffMetadata = + sharedPrefsClient.getRealtimeBackoffMetadata(); Preconditions.checkArgument(realtimeBackoffMetadata.getNumFailedStreams() != NO_FAILED_FETCHES); Preconditions.checkArgument( !realtimeBackoffMetadata.getBackoffEndTime().equals(NO_BACKOFF_TIME)); - metadataClient.resetRealtimeBackoff(); + sharedPrefsClient.resetRealtimeBackoff(); - ConfigMetadataClient.RealtimeBackoffMetadata resetMetadata = - metadataClient.getRealtimeBackoffMetadata(); + ConfigSharedPrefsClient.RealtimeBackoffMetadata resetMetadata = + sharedPrefsClient.getRealtimeBackoffMetadata(); assertThat(resetMetadata.getNumFailedStreams()).isEqualTo(NO_FAILED_FETCHES); assertThat(resetMetadata.getBackoffEndTime()).isEqualTo(NO_BACKOFF_TIME); } @Test public void getBackoffMetadata_isNotSet_returnsNoFailedFetchesAndNotThrottled() { - BackoffMetadata defaultBackoffMetadata = metadataClient.getBackoffMetadata(); + BackoffMetadata defaultBackoffMetadata = sharedPrefsClient.getBackoffMetadata(); assertThat(defaultBackoffMetadata.getNumFailedFetches()).isEqualTo(NO_FAILED_FETCHES); assertThat(defaultBackoffMetadata.getBackoffEndTime()).isEqualTo(NO_BACKOFF_TIME); @@ -206,9 +212,9 @@ public void getBackoffMetadata_isNotSet_returnsNoFailedFetchesAndNotThrottled() public void getBackoffMetadata_hasValues_returnsValues() { int numFailedFetches = 5; Date backoffEndTime = new Date(1000L); - metadataClient.setBackoffMetadata(numFailedFetches, backoffEndTime); + sharedPrefsClient.setBackoffMetadata(numFailedFetches, backoffEndTime); - BackoffMetadata backoffMetadata = metadataClient.getBackoffMetadata(); + BackoffMetadata backoffMetadata = sharedPrefsClient.getBackoffMetadata(); assertThat(backoffMetadata.getNumFailedFetches()).isEqualTo(numFailedFetches); assertThat(backoffMetadata.getBackoffEndTime()).isEqualTo(backoffEndTime); @@ -216,23 +222,23 @@ public void getBackoffMetadata_hasValues_returnsValues() { @Test public void resetBackoff_hasValues_clearsAllValues() { - metadataClient.setBackoffMetadata( + sharedPrefsClient.setBackoffMetadata( /* numFailedFetches= */ 5, /* backoffEndTime= */ new Date(1000L)); - BackoffMetadata backoffMetadata = metadataClient.getBackoffMetadata(); + BackoffMetadata backoffMetadata = sharedPrefsClient.getBackoffMetadata(); Preconditions.checkArgument(backoffMetadata.getNumFailedFetches() != NO_FAILED_FETCHES); Preconditions.checkArgument(!backoffMetadata.getBackoffEndTime().equals(NO_BACKOFF_TIME)); - metadataClient.resetBackoff(); + sharedPrefsClient.resetBackoff(); - BackoffMetadata resetMetadata = metadataClient.getBackoffMetadata(); + BackoffMetadata resetMetadata = sharedPrefsClient.getBackoffMetadata(); assertThat(resetMetadata.getNumFailedFetches()).isEqualTo(NO_FAILED_FETCHES); assertThat(resetMetadata.getBackoffEndTime()).isEqualTo(NO_BACKOFF_TIME); } @Test public void getInfo_hasNoSetValues_returnsDefaults() { - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getFetchTimeMillis()).isEqualTo(LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_NO_FETCH_YET); @@ -245,18 +251,18 @@ public void getInfo_hasNoSetValues_returnsDefaults() { @Test public void getInfo_hasSetValues_returnsValues() { Date lastSuccessfulFetchTime = new Date(1000L); - metadataClient.updateLastFetchAsSuccessfulAt(lastSuccessfulFetchTime); - metadataClient.updateLastFetchAsFailed(); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(lastSuccessfulFetchTime); + sharedPrefsClient.updateLastFetchAsFailed(); long fetchTimeout = 666L; long minimumFetchInterval = 666L; - metadataClient.setConfigSettings( + sharedPrefsClient.setConfigSettings( new FirebaseRemoteConfigSettings.Builder() .setFetchTimeoutInSeconds(fetchTimeout) .setMinimumFetchIntervalInSeconds(minimumFetchInterval) .build()); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getFetchTimeMillis()).isEqualTo(lastSuccessfulFetchTime.getTime()); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); @@ -267,9 +273,9 @@ public void getInfo_hasSetValues_returnsValues() { @Test public void getInfo_firstAndOnlyFetchFails_failStatusAndNoFetchYetTime() { - metadataClient.updateLastFetchAsFailed(); + sharedPrefsClient.updateLastFetchAsFailed(); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); assertThat(info.getFetchTimeMillis()).isEqualTo(LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET); @@ -278,9 +284,9 @@ public void getInfo_firstAndOnlyFetchFails_failStatusAndNoFetchYetTime() { @Test public void getInfo_fetchSucceeds_successStatusAndFetchTimeUpdated() { Date fetchTime = new Date(100L); - metadataClient.updateLastFetchAsSuccessfulAt(fetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(fetchTime); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); assertThat(info.getFetchTimeMillis()).isEqualTo(fetchTime.getTime()); @@ -289,11 +295,11 @@ public void getInfo_fetchSucceeds_successStatusAndFetchTimeUpdated() { @Test public void getInfo_firstFetchSucceedsSecondFetchFails_failStatusAndFirstFetchTime() { Date fetchTime = new Date(100L); - metadataClient.updateLastFetchAsSuccessfulAt(fetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(fetchTime); - metadataClient.updateLastFetchAsFailed(); + sharedPrefsClient.updateLastFetchAsFailed(); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_FAILURE); assertThat(info.getFetchTimeMillis()).isEqualTo(fetchTime.getTime()); @@ -302,12 +308,12 @@ public void getInfo_firstFetchSucceedsSecondFetchFails_failStatusAndFirstFetchTi @Test public void getInfo_twoFetchesSucceed_successStatusAndSecondFetchTime() { Date fetchTime = new Date(100L); - metadataClient.updateLastFetchAsSuccessfulAt(fetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(fetchTime); Date secondFetchTime = new Date(200L); - metadataClient.updateLastFetchAsSuccessfulAt(secondFetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(secondFetchTime); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_SUCCESS); assertThat(info.getFetchTimeMillis()).isEqualTo(secondFetchTime.getTime()); @@ -316,11 +322,11 @@ public void getInfo_twoFetchesSucceed_successStatusAndSecondFetchTime() { @Test public void getInfo_hitsThrottleLimit_throttledStatus() { Date fetchTime = new Date(100L); - metadataClient.updateLastFetchAsSuccessfulAt(fetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(fetchTime); - metadataClient.updateLastFetchAsThrottled(); + sharedPrefsClient.updateLastFetchAsThrottled(); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_THROTTLED); assertThat(info.getFetchTimeMillis()).isEqualTo(fetchTime.getTime()); @@ -329,19 +335,19 @@ public void getInfo_hitsThrottleLimit_throttledStatus() { @Test public void clear_hasSetValues_clearsAll() { Date lastSuccessfulFetchTime = new Date(1000L); - metadataClient.updateLastFetchAsSuccessfulAt(lastSuccessfulFetchTime); + sharedPrefsClient.updateLastFetchAsSuccessfulAt(lastSuccessfulFetchTime); long fetchTimeout = 666L; long minimumFetchInterval = 666L; - metadataClient.setConfigSettings( + sharedPrefsClient.setConfigSettings( new FirebaseRemoteConfigSettings.Builder() .setFetchTimeoutInSeconds(fetchTimeout) .setMinimumFetchIntervalInSeconds(minimumFetchInterval) .build()); - metadataClient.clear(); + sharedPrefsClient.clear(); - FirebaseRemoteConfigInfo info = metadataClient.getInfo(); + FirebaseRemoteConfigInfo info = sharedPrefsClient.getInfo(); assertThat(info.getFetchTimeMillis()).isEqualTo(LAST_FETCH_TIME_IN_MILLIS_NO_FETCH_YET); assertThat(info.getLastFetchStatus()).isEqualTo(LAST_FETCH_STATUS_NO_FETCH_YET); assertThat(info.getConfigSettings().getFetchTimeoutInSeconds()) @@ -349,4 +355,40 @@ public void clear_hasSetValues_clearsAll() { assertThat(info.getConfigSettings().getMinimumFetchIntervalInSeconds()) .isEqualTo(DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS); } + + @Test + public void getCustomSignals_isNotSet_returnsEmptyMap() { + assertThat(sharedPrefsClient.getCustomSignals()).isEqualTo(Collections.emptyMap()); + } + + @Test + public void getCustomSignals_isSet_returnsCustomSignals() { + Map SAMPLE_CUSTOM_SIGNALS = + ImmutableMap.of( + "subscription", "premium", + "age", "20"); + sharedPrefsClient.setCustomSignals(SAMPLE_CUSTOM_SIGNALS); + assertThat(sharedPrefsClient.getCustomSignals()).isEqualTo(SAMPLE_CUSTOM_SIGNALS); + } + + @Test + public void setCustomSignals_multipleTimes_addsNewSignals() { + Map signals1 = ImmutableMap.of("subscription", "premium"); + Map signals2 = ImmutableMap.of("age", "20", "subscription", "basic"); + sharedPrefsClient.setCustomSignals(signals1); + sharedPrefsClient.setCustomSignals(signals2); + Map expectedSignals = ImmutableMap.of("subscription", "basic", "age", "20"); + assertThat(sharedPrefsClient.getCustomSignals()).isEqualTo(expectedSignals); + } + + @Test + public void setCustomSignals_nullValue_removesSignal() { + Map signals1 = ImmutableMap.of("subscription", "premium", "age", "20"); + sharedPrefsClient.setCustomSignals(signals1); + Map signals2 = new HashMap<>(); + signals2.put("age", null); + sharedPrefsClient.setCustomSignals(signals2); + Map expectedSignals = ImmutableMap.of("subscription", "premium"); + assertThat(sharedPrefsClient.getCustomSignals()).isEqualTo(expectedSignals); + } } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt b/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt index c11ea869fbf..2a423843a7c 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt @@ -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 @@ -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) )