Skip to content

Commit e40042a

Browse files
authored
Add updated params to the Realtime listener callback. (#4339)
Add the et updated config parameter keys as a param in the ConfigUpdateListener callback. Keys are added if their presence, value, or metadata has changed. The callback is only called when there is an update; the set should never be empty.
1 parent 993673e commit e40042a

File tree

11 files changed

+543
-56
lines changed

11 files changed

+543
-56
lines changed

firebase-config/api.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// Signature format: 2.0
22
package com.google.firebase.remoteconfig {
33

4+
@com.google.auto.value.AutoValue public abstract class ConfigUpdate {
5+
ctor public ConfigUpdate();
6+
method @NonNull public static com.google.firebase.remoteconfig.ConfigUpdate create(@NonNull java.util.Set<java.lang.String>);
7+
method @NonNull public abstract java.util.Set<java.lang.String> getUpdatedParams();
8+
}
9+
410
public interface ConfigUpdateListener {
511
method public void onError(@NonNull com.google.firebase.remoteconfig.FirebaseRemoteConfigException);
6-
method public void onEvent();
12+
method public void onUpdate(@NonNull com.google.firebase.remoteconfig.ConfigUpdate);
713
}
814

915
public interface ConfigUpdateListenerRegistration {

firebase-config/firebase-config.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ dependencies {
5858
implementation 'com.google.firebase:firebase-measurement-connector:18.0.0'
5959
implementation 'com.google.android.gms:play-services-tasks:18.0.1'
6060

61+
compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6'
6162
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
6263

64+
annotationProcessor 'com.google.auto.value:auto-value:1.6.6'
65+
6366
javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6'
6467

6568
testImplementation 'org.mockito:mockito-core:2.25.0'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
//
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.remoteconfig;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.auto.value.AutoValue;
19+
import java.util.Set;
20+
21+
/**
22+
* Information about the updated config passed to the {@code onUpdate} callback of {@link
23+
* ConfigUpdateListener}.
24+
*/
25+
@AutoValue
26+
public abstract class ConfigUpdate {
27+
@NonNull
28+
public static ConfigUpdate create(@NonNull Set<String> updatedParams) {
29+
return new AutoValue_ConfigUpdate(updatedParams);
30+
}
31+
32+
/**
33+
* Parameter keys whose values have been updated from the currently activated values. Includes
34+
* keys that are added, deleted, and whose value, value source, or metadata has changed.
35+
*/
36+
@NonNull
37+
public abstract Set<String> getUpdatedParams();
38+
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.remoteconfig;
1616

17+
import androidx.annotation.NonNull;
1718
import javax.annotation.Nonnull;
1819

1920
/**
@@ -23,13 +24,16 @@
2324
*/
2425
public interface ConfigUpdateListener {
2526
/**
26-
* Callback for when a new config has been automatically fetched from the backend. Can be used to
27-
* activate the new config.
27+
* Callback for when a new config has been automatically fetched from the backend and has changed
28+
* from the activated config.
29+
*
30+
* @param configUpdate A {@link ConfigUpdate} with information about the updated config, including
31+
* the set of updated parameters.
2832
*/
29-
void onEvent();
33+
void onUpdate(@NonNull ConfigUpdate configUpdate);
3034

3135
/**
32-
* Call back for when an error occurs during Realtime.
36+
* Callback for when an error occurs while listening for or fetching a config update.
3337
*
3438
* @param error
3539
*/

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ protected RemoteConfigComponent(
129129
boolean loadGetDefault) {
130130
this.context = context;
131131
this.executorService = executorService;
132+
this.scheduledExecutorService = scheduledExecutorService;
132133
this.firebaseApp = firebaseApp;
133134
this.firebaseInstallations = firebaseInstallations;
134135
this.firebaseAbt = firebaseAbt;
135136
this.analyticsConnector = analyticsConnector;
136-
this.scheduledExecutorService = scheduledExecutorService;
137137

138138
this.appId = firebaseApp.getOptions().getApplicationId();
139139
GlobalBackgroundListener.ensureBackgroundListenerIsRegistered(context);
@@ -216,7 +216,13 @@ synchronized FirebaseRemoteConfig get(
216216
fetchHandler,
217217
getHandler,
218218
metadataClient,
219-
getRealtime(firebaseApp, firebaseInstallations, fetchHandler, context, namespace));
219+
getRealtime(
220+
firebaseApp,
221+
firebaseInstallations,
222+
fetchHandler,
223+
activatedClient,
224+
context,
225+
namespace));
220226
in.startLoadingConfigsFromDisk();
221227
frcNamespaceInstances.put(namespace, in);
222228
frcNamespaceInstancesStatic.put(namespace, in);
@@ -270,15 +276,16 @@ synchronized ConfigRealtimeHandler getRealtime(
270276
FirebaseApp firebaseApp,
271277
FirebaseInstallationsApi firebaseInstallations,
272278
ConfigFetchHandler configFetchHandler,
279+
ConfigCacheClient activatedCacheClient,
273280
Context context,
274281
String namespace) {
275282
return new ConfigRealtimeHandler(
276283
firebaseApp,
277284
firebaseInstallations,
278285
configFetchHandler,
286+
activatedCacheClient,
279287
context,
280288
namespace,
281-
executorService,
282289
scheduledExecutorService);
283290
}
284291

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import androidx.annotation.VisibleForTesting;
2222
import com.google.android.gms.tasks.Task;
2323
import com.google.android.gms.tasks.Tasks;
24+
import com.google.firebase.remoteconfig.ConfigUpdate;
2425
import com.google.firebase.remoteconfig.ConfigUpdateListener;
2526
import com.google.firebase.remoteconfig.FirebaseRemoteConfigClientException;
2627
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
@@ -30,6 +31,7 @@
3031
import java.io.InputStream;
3132
import java.io.InputStreamReader;
3233
import java.net.HttpURLConnection;
34+
import java.util.HashSet;
3335
import java.util.Random;
3436
import java.util.Set;
3537
import java.util.concurrent.ScheduledExecutorService;
@@ -49,18 +51,21 @@ public class ConfigAutoFetch {
4951
private final HttpURLConnection httpURLConnection;
5052

5153
private final ConfigFetchHandler configFetchHandler;
54+
private final ConfigCacheClient activatedCache;
5255
private final ConfigUpdateListener retryCallback;
5356
private final ScheduledExecutorService scheduledExecutorService;
5457
private final Random random;
5558

5659
public ConfigAutoFetch(
5760
HttpURLConnection httpURLConnection,
5861
ConfigFetchHandler configFetchHandler,
62+
ConfigCacheClient activatedCache,
5963
Set<ConfigUpdateListener> eventListeners,
6064
ConfigUpdateListener retryCallback,
6165
ScheduledExecutorService scheduledExecutorService) {
6266
this.httpURLConnection = httpURLConnection;
6367
this.configFetchHandler = configFetchHandler;
68+
this.activatedCache = activatedCache;
6469
this.eventListeners = eventListeners;
6570
this.retryCallback = retryCallback;
6671
this.scheduledExecutorService = scheduledExecutorService;
@@ -73,9 +78,9 @@ private synchronized void propagateErrors(FirebaseRemoteConfigException exceptio
7378
}
7479
}
7580

76-
private synchronized void executeAllListenerCallbacks() {
81+
private synchronized void executeAllListenerCallbacks(ConfigUpdate configUpdate) {
7782
for (ConfigUpdateListener listener : eventListeners) {
78-
listener.onEvent();
83+
listener.onUpdate(configUpdate);
7984
}
8085
}
8186

@@ -111,7 +116,8 @@ public void listenForNotifications() {
111116
}
112117
}
113118

114-
retryCallback.onEvent();
119+
// TODO: Factor ConfigUpdateListener out of internal retry logic.
120+
retryCallback.onUpdate(ConfigUpdate.create(new HashSet<>()));
115121
scheduledExecutorService.shutdownNow();
116122
try {
117123
scheduledExecutorService.awaitTermination(3L, TimeUnit.SECONDS);
@@ -194,35 +200,78 @@ public void run() {
194200
}
195201

196202
@VisibleForTesting
197-
public synchronized void fetchLatestConfig(int remainingAttempts, long targetVersion) {
198-
int currentAttempts = remainingAttempts - 1;
203+
public synchronized Task<Void> fetchLatestConfig(int remainingAttempts, long targetVersion) {
204+
int remainingAttemptsAfterFetch = remainingAttempts - 1;
205+
int currentAttemptNumber = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch;
199206

200-
// fetchAttemptNumber is calculated by subtracting current attempts from the max number of
201-
// possible attempts.
202207
Task<ConfigFetchHandler.FetchResponse> fetchTask =
203208
configFetchHandler.fetchNowWithTypeAndAttemptNumber(
204-
ConfigFetchHandler.FetchType.REALTIME, MAXIMUM_FETCH_ATTEMPTS - currentAttempts);
205-
fetchTask.onSuccessTask(
206-
(fetchResponse) -> {
207-
long newTemplateVersion = 0;
208-
if (fetchResponse.getFetchedConfigs() != null) {
209-
newTemplateVersion = fetchResponse.getFetchedConfigs().getTemplateVersionNumber();
210-
} else if (fetchResponse.getStatus()
211-
== ConfigFetchHandler.FetchResponse.Status.BACKEND_HAS_NO_UPDATES) {
212-
newTemplateVersion = targetVersion;
213-
}
209+
ConfigFetchHandler.FetchType.REALTIME, currentAttemptNumber);
210+
Task<ConfigContainer> activatedConfigsTask = activatedCache.get();
214211

215-
if (newTemplateVersion >= targetVersion) {
216-
executeAllListenerCallbacks();
217-
} else {
218-
Log.d(
219-
TAG,
220-
"Fetched template version is the same as SDK's current version."
221-
+ " Retrying fetch.");
222-
// Continue fetching until template version number if greater then current.
223-
autoFetch(currentAttempts, targetVersion);
224-
}
225-
return Tasks.forResult(null);
226-
});
212+
return Tasks.whenAllComplete(fetchTask, activatedConfigsTask)
213+
.continueWithTask(
214+
scheduledExecutorService,
215+
(listOfUnusedCompletedTasks) -> {
216+
if (!fetchTask.isSuccessful()) {
217+
return Tasks.forException(
218+
new FirebaseRemoteConfigClientException(
219+
"Failed to auto-fetch config update.", fetchTask.getException()));
220+
}
221+
222+
if (!activatedConfigsTask.isSuccessful()) {
223+
return Tasks.forException(
224+
new FirebaseRemoteConfigClientException(
225+
"Failed to get activated config for auto-fetch",
226+
activatedConfigsTask.getException()));
227+
}
228+
229+
ConfigFetchHandler.FetchResponse fetchResponse = fetchTask.getResult();
230+
ConfigContainer activatedConfigs = activatedConfigsTask.getResult();
231+
232+
if (!fetchResponseIsUpToDate(fetchResponse, targetVersion)) {
233+
Log.d(
234+
TAG,
235+
"Fetched template version is the same as SDK's current version."
236+
+ " Retrying fetch.");
237+
// Continue fetching until template version number is greater then current.
238+
autoFetch(remainingAttemptsAfterFetch, targetVersion);
239+
return Tasks.forResult(null);
240+
}
241+
242+
if (fetchResponse.getFetchedConfigs() == null) {
243+
Log.d(TAG, "The fetch succeeded, but the backend had no updates.");
244+
return Tasks.forResult(null);
245+
}
246+
247+
// Activate hasn't been called yet, so use an empty container for comparison.
248+
// See ConfigCacheClient#get() for details on the async operation.
249+
if (activatedConfigs == null) {
250+
activatedConfigs = ConfigContainer.newBuilder().build();
251+
}
252+
253+
Set<String> updatedParams =
254+
activatedConfigs.getChangedParams(fetchResponse.getFetchedConfigs());
255+
if (updatedParams.isEmpty()) {
256+
Log.d(TAG, "Config was fetched, but no params changed.");
257+
return Tasks.forResult(null);
258+
}
259+
260+
ConfigUpdate configUpdate = ConfigUpdate.create(updatedParams);
261+
executeAllListenerCallbacks(configUpdate);
262+
return Tasks.forResult(null);
263+
});
264+
}
265+
266+
private static Boolean fetchResponseIsUpToDate(
267+
ConfigFetchHandler.FetchResponse response, long lastKnownVersion) {
268+
// If there's a config, make sure its version is >= the last known version.
269+
if (response.getFetchedConfigs() != null) {
270+
return response.getFetchedConfigs().getTemplateVersionNumber() >= lastKnownVersion;
271+
}
272+
273+
// If there isn't a config, return true if the backend had no update.
274+
// Else, it returned an out of date config.
275+
return response.getStatus() == ConfigFetchHandler.FetchResponse.Status.BACKEND_HAS_NO_UPDATES;
227276
}
228277
}

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigContainer.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
package com.google.firebase.remoteconfig.internal;
1616

1717
import java.util.Date;
18+
import java.util.HashSet;
19+
import java.util.Iterator;
1820
import java.util.Map;
21+
import java.util.Set;
1922
import org.json.JSONArray;
2023
import org.json.JSONException;
2124
import org.json.JSONObject;
@@ -154,6 +157,73 @@ public boolean equals(Object o) {
154157
return containerJson.toString().equals(that.toString());
155158
}
156159

160+
/**
161+
* @param other The other {@link ConfigContainer} against which to compute the diff
162+
* @return The set of config keys that have changed between the this config and {@code other}
163+
* @throws JSONException
164+
*/
165+
public Set<String> getChangedParams(ConfigContainer other) throws JSONException {
166+
// Make a copy of the other config before modifying it
167+
JSONObject otherConfig = ConfigContainer.copyOf(other.containerJson).getConfigs();
168+
169+
// Experiments aren't associated with params, so we can just compare arrays once
170+
Boolean experimentsChanged = !this.getAbtExperiments().equals(other.getAbtExperiments());
171+
172+
Set<String> changed = new HashSet<>();
173+
Iterator<String> keys = this.getConfigs().keys();
174+
while (keys.hasNext()) {
175+
String key = keys.next();
176+
177+
// If the ABT Experiments have changed, add all keys since we don't know which keys the ABT
178+
// experiments apply to
179+
if (experimentsChanged) {
180+
changed.add(key);
181+
continue;
182+
}
183+
184+
// If the other config doesn't have the key
185+
if (!other.getConfigs().has(key)) {
186+
changed.add(key);
187+
continue;
188+
}
189+
190+
// If the other config has a different value for the key
191+
if (!this.getConfigs().get(key).equals(other.getConfigs().get(key))) {
192+
changed.add(key);
193+
continue;
194+
}
195+
196+
// If only one of the configs has PersonalizationMetadata for the key
197+
if (this.getPersonalizationMetadata().has(key) && !other.getPersonalizationMetadata().has(key)
198+
|| !this.getPersonalizationMetadata().has(key)
199+
&& other.getPersonalizationMetadata().has(key)) {
200+
changed.add(key);
201+
continue;
202+
}
203+
204+
// If the both configs have PersonalizationMetadata for the key, but the metadata has changed
205+
if (this.getPersonalizationMetadata().has(key)
206+
&& other.getPersonalizationMetadata().has(key)
207+
&& !this.getPersonalizationMetadata()
208+
.get(key)
209+
.equals(other.getPersonalizationMetadata().get(key))) {
210+
changed.add(key);
211+
continue;
212+
}
213+
214+
// Since the key is the same in both configs, remove it from otherConfig
215+
otherConfig.remove(key);
216+
}
217+
218+
// Add all the keys from other that are different
219+
Iterator<String> remainingOtherKeys = otherConfig.keys();
220+
while (remainingOtherKeys.hasNext()) {
221+
changed.add(remainingOtherKeys.next());
222+
}
223+
224+
return changed;
225+
}
226+
157227
@Override
158228
public int hashCode() {
159229
return containerJson.hashCode();

0 commit comments

Comments
 (0)