diff --git a/firebase-installations-interop/firebase-installations-interop.gradle b/firebase-installations-interop/firebase-installations-interop.gradle index 5acad3e1434..69ad5a61a6e 100644 --- a/firebase-installations-interop/firebase-installations-interop.gradle +++ b/firebase-installations-interop/firebase-installations-interop.gradle @@ -40,7 +40,8 @@ android { dependencies { implementation 'com.google.android.gms:play-services-tasks:17.0.0' + implementation project(path: ':firebase-annotations') - compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" } diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java index cf99d3f6318..9178498e4e3 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -16,6 +16,9 @@ import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; +import com.google.firebase.annotations.DeferredApi; +import com.google.firebase.installations.internal.FidListener; +import com.google.firebase.installations.internal.FidListenerHandle; /** * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component @@ -51,4 +54,13 @@ public interface FirebaseInstallationsApi { */ @NonNull Task delete(); + + /** + * Register a listener to receive fid changes. + * + * @param listener implementation of the {@code FidListener} to handle fid changes. + * @hide + */ + @DeferredApi + FidListenerHandle registerFidListener(@NonNull FidListener listener); } diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListener.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListener.java new file mode 100644 index 00000000000..056cf30c61d --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListener.java @@ -0,0 +1,31 @@ +// Copyright 2020 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.installations.internal; + +import androidx.annotation.NonNull; + +/** + * Provides a call-back interface {@link FidListener} that updates on Fid changes. + * + * @hide + */ +public interface FidListener { + /** + * This method gets invoked when a Fid changes. + * + * @param fid represents the newly generated installation id. + */ + void onFidChanged(@NonNull String fid); +} diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListenerHandle.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListenerHandle.java new file mode 100644 index 00000000000..8c49c97d3f2 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/internal/FidListenerHandle.java @@ -0,0 +1,31 @@ +// Copyright 2020 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.installations.internal; + +/** + * Interface for un-registering a previously registered listener {@link FidListener}. + * + * @hide + */ +public interface FidListenerHandle { + + /** + * Unregisters a previously registered {@link FidListener} being tracked by this {@code + * FidListenerHandle}. After the initial call, subsequent calls have no effect. + * + * @hide + */ + void unregister(); +} diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 4e6ebb89db8..78e4549555e 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -7,6 +7,7 @@ package com.google.firebase.installations { method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull com.google.firebase.FirebaseApp); method @NonNull public com.google.android.gms.tasks.Task getToken(boolean); + method @NonNull public com.google.firebase.installations.internal.FidListenerHandle registerFidListener(@NonNull com.google.firebase.installations.internal.FidListener); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 7ff367d7632..91ed0ca7b20 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -28,6 +28,8 @@ import com.google.firebase.heartbeatinfo.HeartBeatInfo; import com.google.firebase.inject.Provider; import com.google.firebase.installations.FirebaseInstallationsException.Status; +import com.google.firebase.installations.internal.FidListener; +import com.google.firebase.installations.internal.FidListenerHandle; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallationEntry; @@ -37,8 +39,10 @@ import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -72,6 +76,9 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { @GuardedBy("this") private String cachedFid; + @GuardedBy("FirebaseInstallations.this") + private Set fidListeners = new HashSet<>(); + @GuardedBy("lock") private final List listeners = new ArrayList<>(); @@ -271,6 +278,25 @@ public Task delete() { return Tasks.call(backgroundExecutor, this::deleteFirebaseInstallationId); } + /** + * Register a callback {@link FidListener} to receive fid changes. + * + * @hide + */ + @NonNull + @Override + public synchronized FidListenerHandle registerFidListener(@NonNull FidListener listener) { + fidListeners.add(listener); + return new FidListenerHandle() { + @Override + public void unregister() { + synchronized (FirebaseInstallations.this) { + fidListeners.remove(listener); + } + } + }; + } + private Task addGetIdListener() { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); StateListener l = new GetIdListener(taskCompletionSource); @@ -356,11 +382,12 @@ private void doNetworkCallIfNecessary(boolean forceRefresh) { // There are two possible cleanup steps to perform at this stage: the FID may need to // be registered with the server or the FID is registered but we need a fresh authtoken. // Registering will also result in a fresh authtoken. Do the appropriate step here. + PersistedInstallationEntry updatedPrefs; try { if (prefs.isErrored() || prefs.isUnregistered()) { - prefs = registerFidWithServer(prefs); + updatedPrefs = registerFidWithServer(prefs); } else if (forceRefresh || utils.isAuthTokenExpired(prefs)) { - prefs = fetchAuthTokenFromServer(prefs); + updatedPrefs = fetchAuthTokenFromServer(prefs); } else { // nothing more to do, get out now return; @@ -371,7 +398,12 @@ private void doNetworkCallIfNecessary(boolean forceRefresh) { } // Store the prefs to persist the result of the previous step. - insertOrUpdatePrefs(prefs); + insertOrUpdatePrefs(updatedPrefs); + + // Update FidListener if a fid has changed. + updateFidListener(prefs, updatedPrefs); + + prefs = updatedPrefs; // Update cachedFID, if FID is successfully REGISTERED and persisted. if (prefs.isRegistered()) { @@ -390,6 +422,17 @@ private void doNetworkCallIfNecessary(boolean forceRefresh) { } } + private synchronized void updateFidListener( + PersistedInstallationEntry prefs, PersistedInstallationEntry updatedPrefs) { + if (fidListeners.size() != 0 + && !prefs.getFirebaseInstallationId().equals(updatedPrefs.getFirebaseInstallationId())) { + // Update all the registered FidListener about fid changes. + for (FidListener listener : fidListeners) { + listener.onFidChanged(updatedPrefs.getFirebaseInstallationId()); + } + } + } + /** * Inserting or Updating the prefs. This operation is made cross-process and cross-thread safe by * wrapping all the processing first in a java synchronization block and wrapping that in a diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FakeFidListener.java b/firebase-installations/src/test/java/com/google/firebase/installations/FakeFidListener.java new file mode 100644 index 00000000000..0d21f8a73e2 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FakeFidListener.java @@ -0,0 +1,31 @@ +// Copyright 2020 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.installations; + +import androidx.annotation.NonNull; +import com.google.firebase.installations.internal.FidListener; + +class FakeFidListener implements FidListener { + private String currentFid; + + @Override + public void onFidChanged(@NonNull String fid) { + currentFid = fid; + } + + public String getLatestFid() { + return currentFid; + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java index 83f60586945..a55334a165b 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -39,6 +39,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.installations.FirebaseInstallationsException.Status; +import com.google.firebase.installations.internal.FidListenerHandle; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; @@ -74,6 +75,7 @@ public class FirebaseInstallationsTest { @Mock private RandomFidGenerator mockFidGenerator; public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + public static final String TEST_FID_2 = "dccccccccccccccccccccd"; public static final String TEST_PROJECT_ID = "777777777777"; @@ -453,6 +455,48 @@ public void testReadToken_withJsonformatting() { assertThat(iidStore.readToken(), equalTo("thetoken")); } + @Test + public void testFidListener_fidChanged_successful() throws Exception { + when(mockIidStore.readIid()).thenReturn(null); + when(mockIidStore.readToken()).thenReturn(null); + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) + .thenReturn( + TEST_INSTALLATION_RESPONSE + .toBuilder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_2) + .setFid(TEST_FID_2) + .build()); + + FakeFidListener fidListener = new FakeFidListener(); + FakeFidListener fidListener2 = new FakeFidListener(); + + // Register the FidListeners + firebaseInstallations.registerFidListener(fidListener); + FidListenerHandle listenerHandle = firebaseInstallations.registerFidListener(fidListener2); + + // Do the actual getId() call under test. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + + // Unregister FidListener2 + listenerHandle.unregister(); + + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_2)); + + // Verify FidListener receives fid changes. + assertThat(fidListener.getLatestFid(), equalTo(TEST_FID_2)); + assertNull(fidListener2.getLatestFid()); + } + @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1);