From 069756e3105668d59a5052acaeb9128c8e32e064 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Fri, 22 Nov 2019 11:15:44 -0800 Subject: [PATCH 1/7] Switch from using SharedPreferences to a flat file --- ...FirebaseInstallationsInstrumentedTest.java | 24 +-- .../local/PersistedInstallationTest.java | 4 +- .../installations/FirebaseInstallations.java | 150 ++++++++++-------- .../local/PersistedInstallation.java | 143 ++++++++++------- .../FirebaseInstallationServiceClient.java | 1 + 5 files changed, 184 insertions(+), 138 deletions(-) diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 26ae5acd6c8..817b9f58408 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -177,7 +177,7 @@ public void setUp() throws FirebaseException { anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_TOKEN_RESULT); - when(persistedInstallationReturnsError.insertOrUpdatePersistedInstallationEntry(any())) + when(persistedInstallationReturnsError.writePreferencesToDisk(any())) .thenReturn(false); when(persistedInstallationReturnsError.readPersistedInstallationEntryValue()) .thenReturn(DEFAULT_PERSISTED_INSTALLATION_ENTRY); @@ -295,7 +295,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { @Test public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { // Update local storage with installation entry that has invalid fid. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(INVALID_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -426,7 +426,7 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E @Test public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { // Update local storage with installation entry that has auth token expired. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); // Mocking unchecked exception on FIS generateAuthToken when(mockClient.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenAnswer( @@ -459,7 +459,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws @Test public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { // Update local storage with installation entry that has auth token expired. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -552,7 +552,7 @@ public void testGetAuthToken_fidExists_successful() throws Exception { @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); @@ -573,7 +573,7 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { // Update local storage with a unregistered installation entry to validate that getAuthToken // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(UNREGISTERED_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -593,7 +593,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exception { // Update local storage with an expired installation entry to ensure that generate auth token // is called. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); // Mocks error during auth token generation when(backendClientReturnsOk.generateAuthToken( anyString(), anyString(), anyString(), anyString())) @@ -664,7 +664,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); @@ -690,7 +690,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( @Test public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() // and verify one task waits for another task to complete. @@ -743,7 +743,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th @Test public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -758,7 +758,7 @@ public void testDelete_registeredFID_successful() throws Exception { @Test public void testDelete_unregisteredFID_successful() throws Exception { // Update local storage with a unregistered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(UNREGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -786,7 +786,7 @@ public void testDelete_emptyPersistedFidEntry_successful() throws Exception { @Test public void testDelete_serverError_failure() throws Exception { // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( executor, diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java index a27e1885924..dbfb91d128a 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java @@ -79,7 +79,7 @@ public void testReadPersistedInstallationEntry_Null() { public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exception { // Insert Persisted Installation Entry with Unregistered status in Shared Prefs assertTrue( - persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + persistedInstallation0.writePreferencesToDisk( PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) @@ -101,7 +101,7 @@ public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exce // Update Persisted Fid Entry with Registered status in Shared Prefs assertTrue( - persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + persistedInstallation0.writePreferencesToDisk( PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) 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 b0aff260fd1..54feb4d18b9 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 @@ -14,6 +14,7 @@ package com.google.firebase.installations; +import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -52,6 +53,7 @@ * */ public class FirebaseInstallations implements FirebaseInstallationsApi { + private static final String TAG = "FirebaseInstallations"; private final FirebaseApp firebaseApp; private final FirebaseInstallationServiceClient serviceClient; @@ -220,21 +222,28 @@ private void triggerOnException( } private final void doRegistration() { + Log.d(TAG, "doRegistration"); try { PersistedInstallationEntry persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); + Log.d(TAG, "status = " + persistedInstallationEntry.getRegistrationStatus()); // New FID needs to be created if (persistedInstallationEntry.isNotGenerated()) { + Log.d(TAG, "need a new FID"); // For a default firebase installation read the existing iid. For other custom firebase // installations create a new fid String fid = readExistingIidOrCreateFid(); + Log.d(TAG, "using fid " + fid); persistFid(fid); + Log.d(TAG, "wrote fid to disk"); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); + Log.d(TAG, "reread entry: " + persistedInstallationEntry); } if (persistedInstallationEntry.isErrored()) { + Log.d(TAG, "in error state, bailing"); throw new FirebaseInstallationsException( persistedInstallationEntry.getFisError(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); @@ -244,57 +253,65 @@ private final void doRegistration() { // FID needs to be registered if (persistedInstallationEntry.isUnregistered()) { - registerAndSaveFid(persistedInstallationEntry); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - // Newly registered Fid will have valid auth token. No refresh required. - synchronized (lock) { - shouldRefreshAuthToken = false; - } + Log.d(TAG, "the fid is unregistered, register it now"); + persistedInstallationEntry = registerAndSaveFid(persistedInstallationEntry); + Log.d(TAG, "registered the fid, entry is now " + persistedInstallationEntry); + // TODO(fredq): I think this is unnecessary, since we check if the token is expired next + // // Newly registered Fid will have valid auth token. No refresh required. + // synchronized (lock) { + // shouldRefreshAuthToken = false; + // } } // Don't notify the listeners at this point; we might as well make ure the auth token is up // to date before letting them know. boolean needRefresh = utils.isAuthTokenExpired(persistedInstallationEntry); + Log.d(TAG, "authtoken expired: " + needRefresh); if (!needRefresh) { synchronized (lock) { needRefresh = shouldRefreshAuthToken; } } - TokenResult tokenResult = null; // Refresh Auth token if needed if (needRefresh) { - tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); + Log.d(TAG, "need to refresh the authtoken"); + TokenResult tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); + Log.d(TAG, "fetched token: " + tokenResult); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); + + // If tokenResult is not null and is not successful, it was cleared due to authentication + // error during auth token generation. + if (!tokenResult.isSuccessful()) { + Log.d(TAG, "fetched token is bad: " + tokenResult); + triggerOnException( + persistedInstallationEntry, + new FirebaseInstallationsException( + "Failed to generate auth token for this Firebase Installation. Call getId() " + + "to recreate a new Fid and a valid auth token.", + FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); + return; + } + synchronized (lock) { shouldRefreshAuthToken = false; } } - // If tokenResult is not null and is not successful, it was cleared due to authentication - // error during auth token generation. - if (tokenResult != null && !tokenResult.isSuccessful()) { - triggerOnException( - persistedInstallationEntry, - new FirebaseInstallationsException( - "Failed to generate auth token for this Firebase Installation. Call getId() " - + "to recreate a new Fid and a valid auth token.", - FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); - return; - } - triggerOnStateReached(persistedInstallationEntry); + Log.d(TAG, "finished doRegistration: " + persistedInstallationEntry); } catch (Exception e) { PersistedInstallationEntry persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); + Log.d(TAG, "doRegistration failed: " + persistedInstallationEntry, e); PersistedInstallationEntry errorInstallationEntry = persistedInstallationEntry .toBuilder() .setFisError(e.getMessage()) .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) .build(); - persistedInstallation.insertOrUpdatePersistedInstallationEntry(errorInstallationEntry); + persistedInstallation.writePreferencesToDisk(errorInstallationEntry); triggerOnException(errorInstallationEntry, e); } } @@ -313,22 +330,15 @@ private String readExistingIidOrCreateFid() { } private void persistFid(String fid) throws FirebaseInstallationsException { - boolean firstUpdateCacheResult = - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.UNREGISTERED) - .build()); - - if (!firstUpdateCacheResult) { - throw new FirebaseInstallationsException( - "Failed to update client side cache.", - FirebaseInstallationsException.Status.CLIENT_ERROR); - } + persistedInstallation.writePreferencesToDisk( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.UNREGISTERED) + .build()); } /** Registers the created Fid with FIS servers and update the shared prefs. */ - private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallationEntry) + private PersistedInstallationEntry registerAndSaveFid(PersistedInstallationEntry entry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); @@ -336,58 +346,60 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation InstallationResponse installationResponse = serviceClient.createFirebaseInstallation( /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), + /*fid= */ entry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), /*appId= */ getApplicationId()); if (installationResponse.getResponseCode() == ResponseCode.OK) { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(installationResponse.getFid()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(installationResponse.getAuthToken().getToken()) - .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); + entry = PersistedInstallationEntry.builder() + .setFirebaseInstallationId(installationResponse.getFid()) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(installationResponse.getAuthToken().getToken()) + .setRefreshToken(installationResponse.getRefreshToken()) + .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) + .setTokenCreationEpochInSecs(creationTime) + .build(); + persistedInstallation.writePreferencesToDisk(entry); } + return entry; - } catch (FirebaseException exception) { - throw new FirebaseInstallationsException( - exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } catch (FirebaseException e) { + throw new FirebaseInstallationsException("error registering fid", + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR, e); } - return null; } /** Calls the FIS servers to generate an auth token for this Firebase installation. */ private TokenResult fetchAuthTokenFromServer( - PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { + PersistedInstallationEntry entry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); TokenResult tokenResult = serviceClient.generateAuthToken( /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), + /*fid= */ entry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*refreshToken= */ persistedInstallationEntry.getRefreshToken()); + /*refreshToken= */ entry.getRefreshToken()); if (tokenResult.isSuccessful()) { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - persistedInstallationEntry - .toBuilder() - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(tokenResult.getToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); - } else { - persistedInstallation.clear(); + entry = entry + .toBuilder() + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(tokenResult.getToken()) + .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) + .setTokenCreationEpochInSecs(creationTime) + .build(); + persistedInstallation.writePreferencesToDisk( + entry); + // TODO(fredq): why clear if there is a network error? + // } else { + // persistedInstallation.clear(); } return tokenResult; - } catch (FirebaseException exception) { + } catch (FirebaseException e) { throw new FirebaseInstallationsException( "Failed to generate auth token for a Firebase Installation.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR, e); } } @@ -396,18 +408,16 @@ private TokenResult fetchAuthTokenFromServer( * storage. */ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException { - - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - - if (persistedInstallationEntry.isRegistered()) { + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + Log.d(TAG, "deleteFirebaseInstallationId: " + entry); + if (entry.isRegistered()) { // Call the FIS servers to delete this Firebase Installation Id. try { serviceClient.deleteFirebaseInstallation( firebaseApp.getOptions().getApiKey(), - persistedInstallationEntry.getFirebaseInstallationId(), + entry.getFirebaseInstallationId(), firebaseApp.getOptions().getProjectId(), - persistedInstallationEntry.getRefreshToken()); + entry.getRefreshToken()); } catch (FirebaseException exception) { throw new FirebaseInstallationsException( diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index fdee274945d..8687f963b66 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -14,19 +14,33 @@ package com.google.firebase.installations.local; -import android.content.Context; +import static java.nio.charset.StandardCharsets.UTF_8; + import android.content.SharedPreferences; -import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Map; +import org.json.JSONException; +import org.json.JSONObject; /** * A layer that locally persists a few Firebase Installation attributes on top the Firebase * Installation API. */ public class PersistedInstallation { + + private final File dataFile; + @NonNull + private final FirebaseApp firebaseApp; + // Registration Status of each persisted fid entry // NOTE: never change the ordinal of the enum values because the enum values are stored in shared // prefs as their ordinal numbers. @@ -55,7 +69,7 @@ public enum RegistrationStatus { REGISTER_ERROR, } - private static final String SHARED_PREFS_NAME = "PersistedInstallation"; + private static final String SETTINGS_FILE_NAME = "PersistedInstallation"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; @@ -75,81 +89,102 @@ public enum RegistrationStatus { PERSISTED_STATUS_KEY, FIS_ERROR_KEY); - @GuardedBy("prefs") - private final SharedPreferences prefs; - private final String persistenceKey; public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path - prefs = - firebaseApp - .getApplicationContext() - .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); persistenceKey = firebaseApp.getPersistenceKey(); + dataFile = new File(firebaseApp.getApplicationContext().getFilesDir(), + SETTINGS_FILE_NAME + "." + persistenceKey + ".json"); + this.firebaseApp = firebaseApp; } @NonNull public PersistedInstallationEntry readPersistedInstallationEntryValue() { - synchronized (prefs) { - String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); - String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); - String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); - long tokenCreationTime = - prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); - long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - String fisError = prefs.getString(getSharedPreferencesKey(FIS_ERROR_KEY), null); - - if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { - return PersistedInstallationEntry.builder().build(); + JSONObject json = readJSONFromFile(); + + String fid = json.optString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = json.optInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); + String authToken = json.optString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); + String refreshToken = json.optString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); + long tokenCreationTime = + json.optLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); + long expiresIn = json.optLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + String fisError = json.optString(getSharedPreferencesKey(FIS_ERROR_KEY), null); + + if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { + return PersistedInstallationEntry.builder().build(); + } + return PersistedInstallationEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .setFisError(fisError) + .build(); + } + + private JSONObject readJSONFromFile() { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] tmpBuf = new byte[16 * 1024]; + try (FileInputStream fis = new FileInputStream(dataFile)) { + while (true) { + int numRead = fis.read(tmpBuf, 0, tmpBuf.length); + if (numRead < 0) { + break; + } + baos.write(tmpBuf, 0, numRead); } - return PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.values()[status]) - .setAuthToken(authToken) - .setRefreshToken(refreshToken) - .setTokenCreationEpochInSecs(tokenCreationTime) - .setExpiresInSecs(expiresIn) - .setFisError(fisError) - .build(); + return new JSONObject(baos.toString()); + } catch (IOException | JSONException e) { + return new JSONObject(); } } - @NonNull - public boolean insertOrUpdatePersistedInstallationEntry( - @NonNull PersistedInstallationEntry entryValue) { - synchronized (prefs) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString( + private void writeJSONToFile(JSONObject prefs) throws IOException { + File tmpFile = File.createTempFile(SETTINGS_FILE_NAME, "tmp", + firebaseApp.getApplicationContext().getFilesDir()); + + FileOutputStream fos = new FileOutputStream(tmpFile); + fos.write(prefs.toString().getBytes()); + fos.close(); + tmpFile.renameTo(dataFile); + } + + public void writePreferencesToDisk(@NonNull PersistedInstallationEntry entryValue) { + try { + JSONObject json = new JSONObject(); + json.put( getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), entryValue.getFirebaseInstallationId()); - editor.putInt( + json.put( getSharedPreferencesKey(PERSISTED_STATUS_KEY), entryValue.getRegistrationStatus().ordinal()); - editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); - editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); - editor.putLong( + json.put(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); + json.put(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); + json.put( getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), entryValue.getTokenCreationEpochInSecs()); - editor.putLong( + json.put( getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); - editor.putString(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); - return editor.commit(); + json.put(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); + writeJSONToFile(json); + } catch (JSONException | IOException e) { + // ignore } } - @NonNull - public boolean clear() { - synchronized (prefs) { - SharedPreferences.Editor editor = prefs.edit(); - for (String k : FID_PREF_KEYS) { - editor.remove(getSharedPreferencesKey(k)); - } - editor.commit(); - return editor.commit(); - } + /** + * Sets the state to NOT_GENERATED. + */ + public void clear() { + writePreferencesToDisk( + PersistedInstallationEntry.builder() + .setRegistrationStatus(RegistrationStatus.NOT_GENERATED) + .build()); } private String getSharedPreferencesKey(String key) { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index bc5947bbe7c..f26aca7eb8d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -144,6 +144,7 @@ private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, firebaseInstallationData.put("fid", fid); firebaseInstallationData.put("appId", appId); firebaseInstallationData.put("authVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + firebaseInstallationData.put("sdkVersion", "t.1.1.0"); return firebaseInstallationData; } From f3abf1e339b0049a1180b0cda3a41a7341f0fddb Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Thu, 5 Dec 2019 14:12:48 -0800 Subject: [PATCH 2/7] Fixes for multi-process access - switch from shared prefs to a flat file - protect generate Fid for cross-process and cross-thread accesses - make doRegistration only read the prefs once at the beginning and clean up the flow - pass the forceRefresh flag into doRegistration rather than storing it in a global --- firebase-installations/api.txt | 30 +- .../firebase/installations/FakeCalendar.java | 77 ++ ...FirebaseInstallationsInstrumentedTest.java | 771 +++++++++--------- .../FisAndroidTestConstants.java | 7 +- .../local/PersistedInstallationTest.java | 45 +- .../installations/FirebaseInstallations.java | 389 ++++----- .../FirebaseInstallationsException.java | 11 +- .../installations/GetAuthTokenListener.java | 13 +- .../firebase/installations/GetIdListener.java | 7 +- .../installations/RandomFidGenerator.java | 84 ++ .../firebase/installations/StateListener.java | 3 +- .../google/firebase/installations/Utils.java | 94 +-- .../local/PersistedInstallation.java | 149 ++-- .../local/PersistedInstallationEntry.java | 66 +- .../FirebaseInstallationServiceClient.java | 201 ++--- .../remote/InstallationResponse.java | 5 +- .../installations/remote/TokenResult.java | 8 +- 17 files changed, 1083 insertions(+), 877 deletions(-) create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 06028ad7b5d..e2359e59dd9 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -17,9 +17,13 @@ package com.google.firebase.installations { } public enum FirebaseInstallationsException.Status { - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status AUTHENTICATION_ERROR; - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status CLIENT_ERROR; - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status SDK_INTERNAL_ERROR; + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status BAD_CONFIG; + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status OK; + } + + public class RandomFidGenerator { + ctor public RandomFidGenerator(); + method @NonNull public String createRandomFid(); } } @@ -33,12 +37,13 @@ package com.google.firebase.installations.local { public class PersistedInstallation { ctor public PersistedInstallation(@NonNull FirebaseApp); - method @NonNull public boolean clear(); - method @NonNull public boolean insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); + method public void clearForTesting(); method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry readPersistedInstallationEntryValue(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry writePreferencesToDisk(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); } public enum PersistedInstallation.RegistrationStatus { + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus ATTEMPT_MIGRATION; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus NOT_GENERATED; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTERED; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTER_ERROR; @@ -59,7 +64,15 @@ package com.google.firebase.installations.local { method public boolean isNotGenerated(); method public boolean isRegistered(); method public boolean isUnregistered(); + method public boolean shouldAttemptMigration(); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder toBuilder(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withAuthToken(@NonNull String, long, long); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withClearedAuthToken(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withFisError(@NonNull String); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withNoGeneratedFid(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withRegisteredFid(@NonNull String, @NonNull String, long, @Nullable String, long); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withUnregisteredFid(@NonNull String); + field @NonNull public static com.google.firebase.installations.local.PersistedInstallationEntry INSTANCE; } public abstract static class PersistedInstallationEntry.Builder { @@ -107,8 +120,8 @@ package com.google.firebase.installations.remote { } public enum InstallationResponse.ResponseCode { + enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode BAD_CONFIG; enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode OK; - enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode SERVER_ERROR; } public abstract class TokenResult { @@ -117,7 +130,6 @@ package com.google.firebase.installations.remote { method @Nullable public abstract com.google.firebase.installations.remote.TokenResult.ResponseCode getResponseCode(); method @Nullable public abstract String getToken(); method @NonNull public abstract long getTokenExpirationTimestamp(); - method public boolean isSuccessful(); method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder toBuilder(); } @@ -130,9 +142,9 @@ package com.google.firebase.installations.remote { } public enum TokenResult.ResponseCode { - enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode FID_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode AUTH_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode BAD_CONFIG; enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode OK; - enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode REFRESH_TOKEN_ERROR; } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java new file mode 100644 index 00000000000..e7802d29bf9 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java @@ -0,0 +1,77 @@ +// Copyright 2019 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 java.util.Calendar; + +public class FakeCalendar extends Calendar { + private long timeInMillis; + + public FakeCalendar() { + timeInMillis = 5000000; + } + + public long getTimeInMillis() { + return timeInMillis; + } + + public void setTimeInMillis(long timeInMillis) { + this.timeInMillis = timeInMillis; + } + + public void advanceTimeBySeconds(long deltaSeconds) { + timeInMillis += (deltaSeconds * 1000L); + } + + @Override + protected void computeTime() { + + } + + @Override + protected void computeFields() { + + } + + @Override + public void add(int i, int i1) { + + } + + @Override + public void roll(int i, boolean b) { + + } + + @Override + public int getMinimum(int i) { + return 0; + } + + @Override + public int getMaximum(int i) { + return 0; + } + + @Override + public int getGreatestMinimum(int i) { + return 0; + } + + @Override + public int getLeastMaximum(int i) { + return 0; + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 817b9f58408..c27b799f525 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -15,16 +15,12 @@ package com.google.firebase.installations; import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_INSTALLATION_ENTRY; -import static com.google.firebase.installations.FisAndroidTestConstants.INVALID_TEST_FID; -import static com.google.firebase.installations.FisAndroidTestConstants.SERVER_ERROR_INSTALLATION_RESPONSE; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_3; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; @@ -33,18 +29,19 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; @@ -54,12 +51,16 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.installations.remote.TokenResult; +import java.io.IOException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -86,13 +87,9 @@ public class FirebaseInstallationsInstrumentedTest { private FirebaseApp firebaseApp; private ExecutorService executor; private PersistedInstallation persistedInstallation; - @Mock private FirebaseInstallationServiceClient backendClientReturnsOk; - @Mock private FirebaseInstallationServiceClient backendClientReturnsError; - @Mock private PersistedInstallation persistedInstallationReturnsError; - @Mock private Utils mockUtils; - @Mock private PersistedInstallation mockPersistedInstallation; - @Mock private FirebaseInstallationServiceClient mockClient; + @Mock private FirebaseInstallationServiceClient mockBackend; @Mock private IidStore mockIidStore; + @Mock private RandomFidGenerator mockFidGenerator; private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder() @@ -104,61 +101,16 @@ public class FirebaseInstallationsInstrumentedTest { .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); - private static final PersistedInstallationEntry REGISTERED_IID_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_INSTANCE_ID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private static final PersistedInstallationEntry EXPIRED_AUTH_TOKEN_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private static final PersistedInstallationEntry UNREGISTERED_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken("") - .setRefreshToken("") - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .build(); - - private static final PersistedInstallationEntry INVALID_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(INVALID_TEST_FID) - .setAuthToken("") - .setRefreshToken("") - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .build(); - - private static final PersistedInstallationEntry UPDATED_AUTH_TOKEN_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN_2) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); + private FirebaseInstallations firebaseInstallations; + private Utils utils; + private FakeCalendar fakeCalendar; @Before - public void setUp() throws FirebaseException { + public void setUp() throws FirebaseException, IOException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + fakeCalendar = new FakeCalendar(); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), @@ -168,69 +120,191 @@ public void setUp() throws FirebaseException { .setApiKey(TEST_API_KEY) .build()); persistedInstallation = new PersistedInstallation(firebaseApp); + persistedInstallation.clearForTesting(); - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - // Mocks successful auth token generation - when(backendClientReturnsOk.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); + utils = new Utils(fakeCalendar); + firebaseInstallations = + new FirebaseInstallations( + executor, + firebaseApp, + mockBackend, + persistedInstallation, + utils, + mockIidStore, + mockFidGenerator); - when(persistedInstallationReturnsError.writePreferencesToDisk(any())) - .thenReturn(false); - when(persistedInstallationReturnsError.readPersistedInstallationEntryValue()) - .thenReturn(DEFAULT_PERSISTED_INSTALLATION_ENTRY); + when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); + } - when(backendClientReturnsError.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenThrow(new FirebaseException("SDK Error")); + @After + public void cleanUp() { + persistedInstallation.clearForTesting(); + try { + executor.awaitTermination(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { - when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); - when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2); + } + } - // Mocks success on FIS deletion - doNothing() - .when(backendClientReturnsOk) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - // Mocks server error on FIS deletion - doThrow(new FirebaseException("Server Error")) - .when(backendClientReturnsError) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + /** + * Check the id generation process when there is no network. There are three cases: + * - no iid -> generate a new fid + * - iid present -> make that iid into a fid + * - fid generated -> return that fid + */ + @Test + public void testGetId_noNetwork_noIid() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(null); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } - @After - public void cleanUp() throws Exception { - persistedInstallation.clear(); + @Test + public void testGetId_noNetwork_iidPresent() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } - private FirebaseInstallations getFirebaseInstallations() { - return new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallation, - mockUtils, - mockIidStore); + @Test + public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withUnregisteredFid("generatedFid")); + + // Do the actual getId() call under test. Confirm that it returns the already generated FID. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo("generatedFid"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid("generatedFid"); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } + /** + * Checks that if we have a registered fid then the fid is returned and no backend calls are made. + */ @Test - public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // No exception, means success. assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = + .isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verifyZeroInteractions(mockBackend); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + } + + /** + * Checks that if we have an unregistered fid that the fid gets registered with the backend + * and no other calls are made. + */ + @Test + public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { + when(mockBackend + .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withUnregisteredFid(TEST_FID_1)); + + // No exception, means success. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second // while we mock fid registration. We dont send an actual request to FIS in tests. executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verify(mockBackend) + .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); + verify(mockBackend, never()) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // check that the fid is still the expected one and is registered PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); @@ -240,16 +314,15 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - when(backendClientReturnsOk.createFirebaseInstallation( + when(mockBackend.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - // No exception, means success. + // Do the actual getId() call under test. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); + .isEqualTo(TEST_INSTANCE_ID_1); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); @@ -257,6 +330,8 @@ public void testGetId_migrateIid_successful() throws Exception { // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // The storage should still have the same ID and the status should indicate that the + // fid si registered. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); @@ -265,11 +340,10 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - when(backendClientReturnsOk.createFirebaseInstallation( + when(mockIidStore.readIid()).thenReturn(null); + when(mockBackend.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getId multiple times Task task1 = firebaseInstallations.getId(); @@ -284,7 +358,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { assertWithMessage("Persisted Fid of Task2 doesn't match.") .that(task2.getResult()) .isEqualTo(TEST_FID_1); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); @@ -292,75 +366,86 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + /** + * Checks that if the server rejects a FID during registration the SDK will use the + * fid in the response as the new fid. + */ @Test - public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { + public void testGetId_unregistered_replacesFidWithResponse() throws Exception { // Update local storage with installation entry that has invalid fid. - persistedInstallation.writePreferencesToDisk(INVALID_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); + when(mockBackend + .createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); - // No exception, means success. + // The first call will return the existing FID, "tobereplaced" assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(INVALID_TEST_FID); + .isEqualTo("tobereplaced"); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - // After FID registration is complete, installation entry is updated with valid fid. - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + // The next call should return the FID that was returned by the server + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); } + /** + * A registration that fails with a SERVER_ERROR will cause the FID to be put into the + * error state. + */ @Test - public void testGetId_PersistedInstallationOk_BackendError() throws Exception { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - persistedInstallation, - mockUtils, - mockIidStore); + public void testGetId_ServerError_UnregisteredFID() throws Exception { + // start with an unregistered fid + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withUnregisteredFid(TEST_FID_1)); - Tasks.await(firebaseInstallations.getId()); + // have the server return a server error for the registration + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); - // Waiting for Task that registers FID on the FIS Servers + // Waiting for Task that registers FID on the FIS Servers. executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // We expect that the server error will cause the FID to be put into the error state. + // There is nothing more we can do. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } + /** + * A registration that fails with an IOException will not cause the FID to be put into the + * error state. + */ @Test - public void testGetId_ServerError_UnregisteredFID() throws Exception { - // Mocking server error on FIS createFirebaseInstallation, returns empty InstallationResponse - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(SERVER_ERROR_INSTALLATION_RESPONSE); + public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { + // set initial state to having an unregistered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // Mocking unchecked exception on FIS createFirebaseInstallation + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); - Tasks.await(firebaseInstallations.getId()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + String fid = Tasks.await(firebaseInstallations.getId()); + assertEquals("fid doesn't match expected", TEST_FID_1, fid); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // We expect that the IOException will cause the request to fail, but it will not + // cause the FID to be put into the error state because we expect this to eventually succeed. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); @@ -368,127 +453,82 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception { } @Test - public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedException { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallationReturnsError, - mockUtils, - mockIidStore); - - // Expect exception - try { - Tasks.await(firebaseInstallations.getId()); - fail("Could not update local storage."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); - } - } - - @Test - public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { - // Mocking unchecked exception on FIS createFirebaseInstallation - when(mockClient.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenAnswer( - invocation -> { - throw new InterruptedException(); - }); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); - - Tasks.await(firebaseInstallations.getId()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); + public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); - // Validate that registration status is REGISTER_ERROR - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); - } + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - @Test - public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { - // Update local storage with installation entry that has auth token expired. - persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); // Mocking unchecked exception on FIS generateAuthToken - when(mockClient.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenAnswer( - invocation -> { - throw new InterruptedException(); - }); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + .isEqualTo(TEST_FID_1); // Waiting for Task that generates auth token with the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - // Validate that registration status is REGISTER_ERROR + // Validate that registration status is still REGISTER PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + /** + * The FID is successfully registered but the token is expired. A getId will cause the token + * to be refreshed in the background. + */ @Test public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { - // Update local storage with installation entry that has auth token expired. - persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + // Get the ID, which should cause the SDK to realize that the auth token is expired and + // kick off a refresh of the token. assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + .isEqualTo(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - // Validate that Persisted FID has a refreshed auth token now - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, never()) + // Check that the token has been refreshed + assertWithMessage("auth token is not what is expected after the refresh") + .that(Tasks.await(firebaseInstallations.getAuthToken( + FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)).getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + + verify(mockBackend, never()) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); PersistedInstallationEntry entryValue = @@ -496,48 +536,13 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); } - @Test - public void testGetAuthToken_PersistedInstallationError_failure() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallationReturnsError, - mockUtils, - mockIidStore); - - // Expect exception - try { - Tasks.await( - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - fail("Could not update local storage."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); - } - } - @Test public void testGetAuthToken_fidExists_successful() throws Exception { - when(mockPersistedInstallation.readPersistedInstallationEntryValue()) - .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - mockPersistedInstallation, - mockUtils, - mockIidStore); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); InstallationTokenResult installationTokenResult = Tasks.await( @@ -546,17 +551,21 @@ public void testGetAuthToken_fidExists_successful() throws Exception { assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); - verify(backendClientReturnsOk, never()) + verify(mockBackend, never()) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); + // start with a registered FID and valid auth token + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // have the server respond with a new token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); InstallationTokenResult installationTokenResult = Tasks.await( @@ -565,18 +574,16 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { // Update local storage with a unregistered installation entry to validate that getAuthToken // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.writePreferencesToDisk(UNREGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withUnregisteredFid(TEST_FID_1)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); InstallationTokenResult installationTokenResult = Tasks.await( @@ -585,68 +592,58 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); - verify(backendClientReturnsOk, times(1)) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); } @Test - public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exception { - // Update local storage with an expired installation entry to ensure that generate auth token - // is called. - persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); + public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE + .withRegisteredFid(TEST_FID_1, TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + // Mocks error during auth token generation - when(backendClientReturnsOk.generateAuthToken( + when(mockBackend.generateAuthToken( anyString(), anyString(), anyString(), anyString())) .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build()); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)) - .thenReturn(/* isExpired*/ true, /*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); // Expect exception try { Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getAuthToken() failed due to Server Error."); + fail("the getAuthToken() call should have failed due to Auth Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.AUTHENTICATION_ERROR); + .isInstanceOf(IOException.class); } - verify(backendClientReturnsOk, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); } + // /** + // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call + // * fails. + // */ @Test public void testGetAuthToken_serverError_failure() throws Exception { - when(mockPersistedInstallation.readPersistedInstallationEntryValue()) - .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(backendClientReturnsError.generateAuthToken( + // start the test with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // have the backend fail when generateAuthToken is invoked. + when(mockBackend.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenThrow(new FirebaseException("Server Error")); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - mockPersistedInstallation, - mockUtils, - mockIidStore); + .thenReturn(TokenResult.builder() + .setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); - // Expect exception + // Make the forced getAuthToken call, which should fail. try { Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getAuthToken() failed due to Server Error."); + fail("getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " + + "returned by the network call."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -654,21 +651,27 @@ public void testGetAuthToken_serverError_failure() throws Exception { .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + .isEqualTo(Status.BAD_CONFIG); } } @Test public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() throws Exception { - // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks - // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads - // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. - persistedInstallation.writePreferencesToDisk(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // expire the authtoken by advancing the clock + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -684,13 +687,19 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( assertWithMessage("Persisted Auth Token doesn't match") .that(task2.getResult().getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() // and verify one task waits for another task to complete. @@ -713,11 +722,8 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setResponseCode(TokenResult.ResponseCode.OK) .build())) - .when(backendClientReturnsOk) + .when(mockBackend) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - when(mockUtils.isAuthTokenExpired(any())).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getAuthToken multiple times with FORCE_REFRESH option. Task task1 = @@ -733,7 +739,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th assertWithMessage("Persisted Auth Token doesn't match") .that(task2.getResult().getToken()) .isEqualTo(TEST_AUTH_TOKEN_3); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); @@ -743,63 +749,62 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th @Test public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry - persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, times(1)) + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, times(1)) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testDelete_unregisteredFID_successful() throws Exception { // Update local storage with a unregistered installation entry - persistedInstallation.writePreferencesToDisk(UNREGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, never()) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @Test public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, never()) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @Test - public void testDelete_serverError_failure() throws Exception { + public void testDelete_serverError_badConfig() throws Exception { // Update local storage with a registered installation entry - persistedInstallation.writePreferencesToDisk(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - persistedInstallation, - mockUtils, - mockIidStore); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + + doThrow(new FirebaseException("Server Error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); // Expect exception try { Tasks.await(firebaseInstallations.delete()); - fail("delete() failed due to Server Error."); + fail("firebaseInstallations.delete() failed due to Server Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -807,7 +812,31 @@ public void testDelete_serverError_failure() throws Exception { .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + .isEqualTo(Status.BAD_CONFIG); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); + } + } + + @Test + public void testDelete_networkError() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + + doThrow(new IOException()) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + Tasks.await(firebaseInstallations.delete()); + fail("firebaseInstallations.delete() failed due to a Network Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index eb4a0f47f85..2080a55aff8 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -21,8 +21,6 @@ public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; - // Invalid FID. - public static final String INVALID_TEST_FID = "invalid"; public static final String TEST_PROJECT_ID = "777777777777"; @@ -38,8 +36,7 @@ public final class FisAndroidTestConstants { public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; public static final String TEST_APP_ID_2 = "1:987654321:android:abcdef"; - public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 1000L; - public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP_2 = 2000L; + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 4000L; public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; @@ -81,6 +78,4 @@ public final class FisAndroidTestConstants { .setResponseCode(TokenResult.ResponseCode.OK) .build(); - public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE = - InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java index dbfb91d128a..53f39fe3a58 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java @@ -24,7 +24,6 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; -import static org.junit.Assert.assertTrue; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; @@ -62,9 +61,9 @@ public void setUp() { } @After - public void cleanUp() throws Exception { - persistedInstallation0.clear(); - persistedInstallation1.clear(); + public void cleanUp() { + persistedInstallation0.clearForTesting(); + persistedInstallation1.clearForTesting(); } @Test @@ -78,16 +77,15 @@ public void testReadPersistedInstallationEntry_Null() { @Test public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exception { // Insert Persisted Installation Entry with Unregistered status in Shared Prefs - assertTrue( - persistedInstallation0.writePreferencesToDisk( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build())); + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()); PersistedInstallationEntry entryValue = persistedInstallation0.readPersistedInstallationEntryValue(); @@ -100,16 +98,15 @@ public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exce assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_1); // Update Persisted Fid Entry with Registered status in Shared Prefs - assertTrue( - persistedInstallation0.writePreferencesToDisk( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build())); + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()); entryValue = persistedInstallation0.readPersistedInstallationEntryValue(); // Validate update was successful 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 54feb4d18b9..705753858d6 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 @@ -14,26 +14,29 @@ package com.google.firebase.installations; -import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; -import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.installations.remote.TokenResult; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.util.ArrayList; +import java.util.Calendar; import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; @@ -53,22 +56,23 @@ * */ public class FirebaseInstallations implements FirebaseInstallationsApi { - private static final String TAG = "FirebaseInstallations"; - private final FirebaseApp firebaseApp; private final FirebaseInstallationServiceClient serviceClient; private final PersistedInstallation persistedInstallation; private final ExecutorService executor; private final Utils utils; private final IidStore iidStore; + private final RandomFidGenerator fidGenerator; private final Object lock = new Object(); - @GuardedBy("lock") - private boolean shouldRefreshAuthToken; - @GuardedBy("lock") private final List listeners = new ArrayList<>(); + /* used for thread-level synchronization of generating and persisting fids */ + private final Object lockGenerateFid = new Object(); + /* file used for process-level syncronization of generating and persisting fids */ + private static final String LOCKFILE_NAME_GENERATE_FID = "generatefid.lock"; + /** package private constructor. */ FirebaseInstallations(FirebaseApp firebaseApp) { this( @@ -76,8 +80,9 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { firebaseApp, new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), new PersistedInstallation(firebaseApp), - new Utils(DefaultClock.getInstance()), - new IidStore()); + new Utils(Calendar.getInstance()), + new IidStore(), + new RandomFidGenerator()); } FirebaseInstallations( @@ -86,13 +91,15 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { FirebaseInstallationServiceClient serviceClient, PersistedInstallation persistedInstallation, Utils utils, - IidStore iidStore) { + IidStore iidStore, + RandomFidGenerator fidGenerator) { this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; this.persistedInstallation = persistedInstallation; this.utils = utils; this.iidStore = iidStore; + this.fidGenerator = fidGenerator; } /** @@ -138,7 +145,7 @@ String getName() { @Override public Task getId() { Task task = addGetIdListener(); - executor.execute(this::doRegistration); + executor.execute(this::doGetId); return task; } @@ -154,8 +161,12 @@ public Task getId() { @NonNull @Override public Task getAuthToken(@AuthTokenOption int authTokenOption) { - Task task = addGetAuthTokenListener(authTokenOption); - executor.execute(this::doRegistration); + Task task = addGetAuthTokenListener(); + if (authTokenOption == FORCE_REFRESH) { + executor.execute(this::doGetAuthTokenForceRefresh); + } else { + executor.execute(this::doGetAuthTokenWithoutForceRefresh); + } return task; } @@ -179,15 +190,11 @@ private Task addGetIdListener() { return taskCompletionSource.getTask(); } - private Task addGetAuthTokenListener( - @AuthTokenOption int authTokenOption) { + private Task addGetAuthTokenListener() { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); StateListener l = new GetAuthTokenListener(utils, taskCompletionSource); synchronized (lock) { - if (authTokenOption == FORCE_REFRESH) { - shouldRefreshAuthToken = true; - } listeners.add(l); } return taskCompletionSource.getTask(); @@ -198,8 +205,7 @@ private void triggerOnStateReached(PersistedInstallationEntry persistedInstallat Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = - l.onStateReached(persistedInstallationEntry, shouldRefreshAuthToken); + boolean doneListening = l.onStateReached(persistedInstallationEntry); if (doneListening) { it.remove(); } @@ -207,13 +213,12 @@ private void triggerOnStateReached(PersistedInstallationEntry persistedInstallat } } - private void triggerOnException( - PersistedInstallationEntry persistedInstallationEntry, Exception exception) { + private void triggerOnException(PersistedInstallationEntry prefs, Exception exception) { synchronized (lock) { Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = l.onException(persistedInstallationEntry, exception); + boolean doneListening = l.onException(prefs, exception); if (doneListening) { it.remove(); } @@ -221,185 +226,202 @@ private void triggerOnException( } } - private final void doRegistration() { - Log.d(TAG, "doRegistration"); - try { - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - Log.d(TAG, "status = " + persistedInstallationEntry.getRegistrationStatus()); - - // New FID needs to be created - if (persistedInstallationEntry.isNotGenerated()) { - Log.d(TAG, "need a new FID"); - - // For a default firebase installation read the existing iid. For other custom firebase - // installations create a new fid - String fid = readExistingIidOrCreateFid(); - Log.d(TAG, "using fid " + fid); - persistFid(fid); - Log.d(TAG, "wrote fid to disk"); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - Log.d(TAG, "reread entry: " + persistedInstallationEntry); - } + private final void doGetId() { + doRegistrationInternal(false); + } - if (persistedInstallationEntry.isErrored()) { - Log.d(TAG, "in error state, bailing"); - throw new FirebaseInstallationsException( - persistedInstallationEntry.getFisError(), - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - } + private final void doGetAuthTokenWithoutForceRefresh() { + doRegistrationInternal(false); + } - triggerOnStateReached(persistedInstallationEntry); - - // FID needs to be registered - if (persistedInstallationEntry.isUnregistered()) { - Log.d(TAG, "the fid is unregistered, register it now"); - persistedInstallationEntry = registerAndSaveFid(persistedInstallationEntry); - Log.d(TAG, "registered the fid, entry is now " + persistedInstallationEntry); - // TODO(fredq): I think this is unnecessary, since we check if the token is expired next - // // Newly registered Fid will have valid auth token. No refresh required. - // synchronized (lock) { - // shouldRefreshAuthToken = false; - // } - } + private final void doGetAuthTokenForceRefresh() { + doRegistrationInternal(true); + } - // Don't notify the listeners at this point; we might as well make ure the auth token is up - // to date before letting them know. + /** + * Logic for handling get id and the two forms of get auth token. This handles all the work, + * including creating a new FID if one hasn't been generated yet and making the network + * calls to create an installation and to retrieve a new auth token. Also contains the + * error handling for when the server says that credentials are bad and that a new Fid + * needs to be generated. + * @param forceRefresh true if this is for a getAuthToken call and if the caller wants + * to fetch a new auth token from the server even if an unexpired auth token exists + * on the client. + */ + private final void doRegistrationInternal(boolean forceRefresh) { + PersistedInstallationEntry prefs = persistedInstallation.readPersistedInstallationEntryValue(); + + // Check if a new FID needs to be created + if (prefs.isNotGenerated()) { + // For a default firebase installation read the existing iid. For other custom firebase + // installations create a new fid + prefs = generateAndPersistFidProcessSafe(prefs); + } - boolean needRefresh = utils.isAuthTokenExpired(persistedInstallationEntry); - Log.d(TAG, "authtoken expired: " + needRefresh); - if (!needRefresh) { - synchronized (lock) { - needRefresh = shouldRefreshAuthToken; - } + // Since the caller wants to force an authtoken refresh remove the authtoken from the + // prefs we are working with, so the following steps know a new token is required. + if (forceRefresh) { + prefs = prefs.withClearedAuthToken(); + } + + triggerOnStateReached(prefs); + + // 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. + try { + if (prefs.isErrored() || prefs.isUnregistered()) { + prefs = registerFidWithServer(prefs); + } else if (forceRefresh || utils.isAuthTokenExpired(prefs)) { + prefs = fetchAuthTokenFromServer(prefs); + } else { + // nothing more to do, get out now + return; } + } catch (IOException e) { + triggerOnException(prefs, e); + return; + } - // Refresh Auth token if needed - if (needRefresh) { - Log.d(TAG, "need to refresh the authtoken"); - TokenResult tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); - Log.d(TAG, "fetched token: " + tokenResult); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - - // If tokenResult is not null and is not successful, it was cleared due to authentication - // error during auth token generation. - if (!tokenResult.isSuccessful()) { - Log.d(TAG, "fetched token is bad: " + tokenResult); - triggerOnException( - persistedInstallationEntry, - new FirebaseInstallationsException( - "Failed to generate auth token for this Firebase Installation. Call getId() " - + "to recreate a new Fid and a valid auth token.", - FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); - return; - } + // Store the prefs to persist the result of the previous step. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(prefs); + + // Let the caller know about the result. + if (prefs.isErrored()) { + triggerOnException(prefs, new FirebaseInstallationsException(Status.BAD_CONFIG)); + } else if (prefs.isNotGenerated()) { + // If there is no fid it means the call failed with an auth error. Simulate an + // IOException so that the caller knows to try again. + triggerOnException(prefs, new IOException("cleared fid due to auth error")); + } else { + triggerOnStateReached(prefs); + } + } - synchronized (lock) { - shouldRefreshAuthToken = false; - } + /** + * Generate a new FID, either from an existing IID or generated randomly. + * If an IID exists and this is the first time a FID has been generated for this + * installation, the IID will be used as the FID. If the FID is ever cleared then + * the next time a FID is generated the IID is ignored and a FID is generated + * randomly. + *

+ * This method ensures that only one thread in one process will run this code at a + * time, so that two different processes or threads don't create two different FIDs. + * @param prefs takes the current state of the prefs + * @return a new version of the prefs that includes the new FID. These prefs will have already + * been persisted. + */ + private PersistedInstallationEntry generateAndPersistFidProcessSafe( + PersistedInstallationEntry prefs) { + FileLock fileLock = getCrossProcessLock(); + try { + synchronized (lockGenerateFid) { + // Only one single thread from one single process can execute this block + // at any given time. + String fid = readExistingIidOrCreateFid(prefs); + prefs = persistedInstallation.insertOrUpdatePersistedInstallationEntry( + prefs.withUnregisteredFid(fid)); + return prefs; } + } finally { + releaseCrossProcessLock(fileLock); + } + } - triggerOnStateReached(persistedInstallationEntry); - Log.d(TAG, "finished doRegistration: " + persistedInstallationEntry); - } catch (Exception e) { - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - Log.d(TAG, "doRegistration failed: " + persistedInstallationEntry, e); - PersistedInstallationEntry errorInstallationEntry = - persistedInstallationEntry - .toBuilder() - .setFisError(e.getMessage()) - .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) - .build(); - persistedInstallation.writePreferencesToDisk(errorInstallationEntry); - triggerOnException(errorInstallationEntry, e); + /** + * Use file locking to acquire a lock that will also block other processes. + */ + private FileLock getCrossProcessLock() { + try { + File file = new File(firebaseApp.getApplicationContext().getFilesDir(), + LOCKFILE_NAME_GENERATE_FID); + FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); + // Use the file channel to create a lock on the file. + // This method blocks until it can retrieve the lock. + return channel.lock(); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); } } - private String readExistingIidOrCreateFid() { + /** + * Release a previously acquired lock. + */ + private void releaseCrossProcessLock(FileLock fileLock) { + try { + fileLock.release(); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); + } + } + + private String readExistingIidOrCreateFid(PersistedInstallationEntry prefs) { // Check if this firebase app is the default (first initialized) instance - if (!firebaseApp.equals(FirebaseApp.getInstance())) { - return utils.createRandomFid(); + if (!firebaseApp.equals(FirebaseApp.getInstance()) || !prefs.shouldAttemptMigration()) { + return fidGenerator.createRandomFid(); } // For a default firebase installation, read the existing iid from shared prefs String fid = iidStore.readIid(); if (fid == null) { - fid = utils.createRandomFid(); + fid = fidGenerator.createRandomFid(); } return fid; } - private void persistFid(String fid) throws FirebaseInstallationsException { - persistedInstallation.writePreferencesToDisk( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.UNREGISTERED) - .build()); - } - - /** Registers the created Fid with FIS servers and update the shared prefs. */ - private PersistedInstallationEntry registerAndSaveFid(PersistedInstallationEntry entry) - throws FirebaseInstallationsException { - try { - long creationTime = utils.currentTimeInSecs(); - - InstallationResponse installationResponse = - serviceClient.createFirebaseInstallation( - /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ entry.getFirebaseInstallationId(), - /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*appId= */ getApplicationId()); - if (installationResponse.getResponseCode() == ResponseCode.OK) { - entry = PersistedInstallationEntry.builder() - .setFirebaseInstallationId(installationResponse.getFid()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(installationResponse.getAuthToken().getToken()) - .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build(); - persistedInstallation.writePreferencesToDisk(entry); - } - return entry; - - } catch (FirebaseException e) { - throw new FirebaseInstallationsException("error registering fid", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR, e); + /** Registers the created Fid with FIS servers and update the persisted state. */ + private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEntry prefs) + throws IOException { + InstallationResponse response = + serviceClient.createFirebaseInstallation( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ prefs.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*appId= */ getApplicationId()); + + switch(response.getResponseCode()) { + case OK: + return prefs.withRegisteredFid( + response.getFid(), + response.getRefreshToken(), + utils.currentTimeInSecs(), + response.getAuthToken().getToken(), + response.getAuthToken().getTokenExpirationTimestamp()); + case BAD_CONFIG: + return prefs.withFisError("BAD CONFIG"); + default: + throw new IOException(); } } - /** Calls the FIS servers to generate an auth token for this Firebase installation. */ - private TokenResult fetchAuthTokenFromServer( - PersistedInstallationEntry entry) throws FirebaseInstallationsException { - try { - long creationTime = utils.currentTimeInSecs(); - TokenResult tokenResult = - serviceClient.generateAuthToken( - /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ entry.getFirebaseInstallationId(), - /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*refreshToken= */ entry.getRefreshToken()); - - if (tokenResult.isSuccessful()) { - entry = entry - .toBuilder() - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(tokenResult.getToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build(); - persistedInstallation.writePreferencesToDisk( - entry); - // TODO(fredq): why clear if there is a network error? - // } else { - // persistedInstallation.clear(); - } - return tokenResult; - - } catch (FirebaseException e) { - throw new FirebaseInstallationsException( - "Failed to generate auth token for a Firebase Installation.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR, e); + /** + * Calls the FIS servers to generate an auth token for this Firebase installation. Returns a + * PersistedInstallationEntry with the new authtoken. The authtoken in the returned + * PersistedInstallationEntry will be "expired" if the server refuses to generate an auth token + * for the fid. + */ + private PersistedInstallationEntry fetchAuthTokenFromServer( + @NonNull PersistedInstallationEntry prefs) throws IOException { + TokenResult tokenResult = + serviceClient.generateAuthToken( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ prefs.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*refreshToken= */ prefs.getRefreshToken()); + + switch(tokenResult.getResponseCode()) { + case OK: + return prefs.withAuthToken( + tokenResult.getToken(), + tokenResult.getTokenExpirationTimestamp(), + utils.currentTimeInSecs()); + case BAD_CONFIG: + return prefs.withFisError("BAD CONFIG"); + case AUTH_ERROR: + // The the server refused to generate a new auth token due to bad credentials, clear the + // FID to force the generation of a new one. + return prefs.withNoGeneratedFid(); + default: + throw new IOException(); } } @@ -407,9 +429,8 @@ private TokenResult fetchAuthTokenFromServer( * Deletes the firebase installation id of the {@link FirebaseApp} from FIS servers and local * storage. */ - private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException { + private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException, IOException { PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); - Log.d(TAG, "deleteFirebaseInstallationId: " + entry); if (entry.isRegistered()) { // Call the FIS servers to delete this Firebase Installation Id. try { @@ -422,11 +443,11 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to delete a Firebase Installation.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + Status.BAD_CONFIG); } } - persistedInstallation.clear(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(entry.withNoGeneratedFid()); return null; } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index 4309a782ffd..ad82366ed0d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -19,13 +19,14 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ public class FirebaseInstallationsException extends FirebaseException { - // TODO(ankitagj): Improve exception handling and java doc public enum Status { - SDK_INTERNAL_ERROR, + OK, - CLIENT_ERROR, - - AUTHENTICATION_ERROR + /** + * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key + * or Project. + */ + BAD_CONFIG, } @NonNull private final Status status; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index bc36a20bac6..52dd3c46989 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -22,18 +22,17 @@ class GetAuthTokenListener implements StateListener { private final TaskCompletionSource resultTaskCompletionSource; public GetAuthTokenListener( - Utils utils, TaskCompletionSource resultTaskCompletionSource) { + Utils utils, + TaskCompletionSource resultTaskCompletionSource) { this.utils = utils; this.resultTaskCompletionSource = resultTaskCompletionSource; } @Override - public boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken) { + public boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry) { // AuthTokenListener state is reached when FID is registered and has a valid auth token if (persistedInstallationEntry.isRegistered() - && !utils.isAuthTokenExpired(persistedInstallationEntry) - && !shouldRefreshAuthToken) { + && !utils.isAuthTokenExpired(persistedInstallationEntry)) { resultTaskCompletionSource.setResult( InstallationTokenResult.builder() .setToken(persistedInstallationEntry.getAuthToken()) @@ -48,7 +47,9 @@ public boolean onStateReached( @Override public boolean onException( PersistedInstallationEntry persistedInstallationEntry, Exception exception) { - if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { + if (persistedInstallationEntry.isErrored() + || persistedInstallationEntry.isNotGenerated() + || persistedInstallationEntry.isUnregistered()) { resultTaskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java index 38134cc6217..44f534f7da1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java @@ -25,9 +25,10 @@ public GetIdListener(TaskCompletionSource taskCompletionSource) { } @Override - public boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean unused) { - if (persistedInstallationEntry.isUnregistered() || persistedInstallationEntry.isRegistered()) { + public boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry) { + if (persistedInstallationEntry.isUnregistered() + || persistedInstallationEntry.isRegistered() + || persistedInstallationEntry.isErrored()) { taskCompletionSource.trySetResult(persistedInstallationEntry.getFirebaseInstallationId()); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java new file mode 100644 index 00000000000..b06fbe6fb5e --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java @@ -0,0 +1,84 @@ +// Copyright 2019 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 java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.UUID; + +public class RandomFidGenerator { + /** + * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this + * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy + * Instance-IDs + */ + private static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); + + /** + * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND + * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. + */ + private static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); + + /** Length of new-format FIDs as introduced in 2019. */ + private static final int FID_LENGTH = 22; + + /** + * Creates a random FID of valid format without checking if the FID is already in use by any + * Firebase Installation. + * + *

Note: Even though this method does not check with the FIS database if the returned FID is + * already in use, the probability of collision is extremely and negligibly small! + * + * @return random FID value + */ + @NonNull + public String createRandomFid() { + // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. + byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); + uuidBytes[16] = uuidBytes[0]; + uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); + return encodeFidBase64UrlSafe(uuidBytes); + } + + /** + * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded + * String-representation. + * + *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is + * recommended to deliver a byte-array containing at least 16.5 bytes. + * + * @param rawValue FID value to be encoded + * @return (22-character or shorter) String containing the base64-encoded value + */ + private static String encodeFidBase64UrlSafe(byte[] rawValue) { + return new String( + android.util.Base64.encode( + rawValue, + android.util.Base64.URL_SAFE + | android.util.Base64.NO_PADDING + | android.util.Base64.NO_WRAP), + Charset.defaultCharset()) + .substring(0, FID_LENGTH); + } + + private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { + ByteBuffer bb = ByteBuffer.wrap(output); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java index d9570835adf..8cd08bdb299 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java @@ -21,8 +21,7 @@ interface StateListener { * Returns {@code true} if the defined {@link PersistedInstallationEntry} state is reached, {@code * false} otherwise. */ - boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken); + boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry); /** * Returns {@code true} if an exception is thrown while registering a Firebase Installation, diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index 22bf2576ea5..d2f748ce734 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -14,99 +14,37 @@ package com.google.firebase.installations; -import androidx.annotation.NonNull; -import com.google.android.gms.common.util.Clock; +import android.text.TextUtils; import com.google.firebase.installations.local.PersistedInstallationEntry; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.UUID; +import java.util.Calendar; import java.util.concurrent.TimeUnit; /** Util methods used for {@link FirebaseInstallations} */ class Utils { + public static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1); + private final Calendar calendar; - private final Clock clock; - /** - * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this - * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy - * Instance-IDs - */ - public static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); - - /** - * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND - * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. - */ - public static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); - - /** Length of new-format FIDs as introduced in 2019. */ - public static final int FID_LENGTH = 22; - - private static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1); - - Utils(Clock clock) { - this.clock = clock; + Utils(Calendar calendar) { + this.calendar = calendar; } /** * Checks if the FIS Auth token is expired or going to expire in next 1 hour {@link * #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}. */ - public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallationEntry) { - return persistedInstallationEntry.isRegistered() - && persistedInstallationEntry.getTokenCreationEpochInSecs() - + persistedInstallationEntry.getExpiresInSecs() - < currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + public boolean isAuthTokenExpired(PersistedInstallationEntry entry) { + if (TextUtils.isEmpty(entry.getAuthToken())) { + return true; + } + if ((entry.getTokenCreationEpochInSecs() + entry.getExpiresInSecs()) + < (currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS)) { + return true; + } + return false; } /** Returns current time in seconds. */ public long currentTimeInSecs() { - return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); - } - - /** - * Creates a random FID of valid format without checking if the FID is already in use by any - * Firebase Installation. - * - *

Note: Even though this method does not check with the FIS database if the returned FID is - * already in use, the probability of collision is extremely and negligibly small! - * - * @return random FID value - */ - @NonNull - public String createRandomFid() { - // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. - byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); - uuidBytes[16] = uuidBytes[0]; - uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); - return encodeFidBase64UrlSafe(uuidBytes); - } - - /** - * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded - * String-representation. - * - *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is - * recommended to deliver a byte-array containing at least 16.5 bytes. - * - * @param rawValue FID value to be encoded - * @return (22-character or shorter) String containing the base64-encoded value - */ - private static String encodeFidBase64UrlSafe(byte[] rawValue) { - return new String( - android.util.Base64.encode( - rawValue, - android.util.Base64.URL_SAFE - | android.util.Base64.NO_PADDING - | android.util.Base64.NO_WRAP), - Charset.defaultCharset()) - .substring(0, FID_LENGTH); - } - - private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { - ByteBuffer bb = ByteBuffer.wrap(output); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - return bb.array(); + return TimeUnit.MILLISECONDS.toSeconds(calendar.getTimeInMillis()); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index 8687f963b66..ac4ac34facd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -14,20 +14,13 @@ package com.google.firebase.installations.local; -import static java.nio.charset.StandardCharsets.UTF_8; - -import android.content.SharedPreferences; import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; import org.json.JSONException; import org.json.JSONObject; @@ -36,15 +29,18 @@ * Installation API. */ public class PersistedInstallation { - private final File dataFile; - @NonNull - private final FirebaseApp firebaseApp; + @NonNull private final FirebaseApp firebaseApp; // Registration Status of each persisted fid entry // NOTE: never change the ordinal of the enum values because the enum values are stored in shared // prefs as their ordinal numbers. public enum RegistrationStatus { + /** + * {@link PersistedInstallationEntry} legacy registration status. Next state: UNREGISTERED - A + * new FID is created and persisted locally before registering with FIS servers. + */ + ATTEMPT_MIGRATION, /** * {@link PersistedInstallationEntry} default registration status. Next state: UNREGISTERED - A * new FID is created and persisted locally before registering with FIS servers. @@ -79,24 +75,16 @@ public enum RegistrationStatus { private static final String PERSISTED_STATUS_KEY = "Status"; private static final String FIS_ERROR_KEY = "FisError"; - private static final List FID_PREF_KEYS = - Arrays.asList( - FIREBASE_INSTALLATION_ID_KEY, - AUTH_TOKEN_KEY, - REFRESH_TOKEN_KEY, - TOKEN_CREATION_TIME_IN_SECONDS_KEY, - EXPIRES_IN_SECONDS_KEY, - PERSISTED_STATUS_KEY, - FIS_ERROR_KEY); - private final String persistenceKey; public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path persistenceKey = firebaseApp.getPersistenceKey(); - dataFile = new File(firebaseApp.getApplicationContext().getFilesDir(), - SETTINGS_FILE_NAME + "." + persistenceKey + ".json"); + dataFile = + new File( + firebaseApp.getApplicationContext().getFilesDir(), + SETTINGS_FILE_NAME + "." + persistenceKey + ".json"); this.firebaseApp = firebaseApp; } @@ -104,27 +92,25 @@ public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { public PersistedInstallationEntry readPersistedInstallationEntryValue() { JSONObject json = readJSONFromFile(); - String fid = json.optString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = json.optInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); - String authToken = json.optString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); - String refreshToken = json.optString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); - long tokenCreationTime = - json.optLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); - long expiresIn = json.optLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - String fisError = json.optString(getSharedPreferencesKey(FIS_ERROR_KEY), null); - - if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { - return PersistedInstallationEntry.builder().build(); - } - return PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.values()[status]) - .setAuthToken(authToken) - .setRefreshToken(refreshToken) - .setTokenCreationEpochInSecs(tokenCreationTime) - .setExpiresInSecs(expiresIn) - .setFisError(fisError) - .build(); + String fid = json.optString(FIREBASE_INSTALLATION_ID_KEY, null); + int status = json.optInt(PERSISTED_STATUS_KEY, RegistrationStatus.ATTEMPT_MIGRATION.ordinal()); + String authToken = json.optString(AUTH_TOKEN_KEY, null); + String refreshToken = json.optString(REFRESH_TOKEN_KEY, null); + long tokenCreationTime = json.optLong(TOKEN_CREATION_TIME_IN_SECONDS_KEY, 0); + long expiresIn = json.optLong(EXPIRES_IN_SECONDS_KEY, 0); + String fisError = json.optString(FIS_ERROR_KEY, null); + + PersistedInstallationEntry prefs = + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .setFisError(fisError) + .build(); + return prefs; } private JSONObject readJSONFromFile() { @@ -144,50 +130,51 @@ private JSONObject readJSONFromFile() { } } - private void writeJSONToFile(JSONObject prefs) throws IOException { - File tmpFile = File.createTempFile(SETTINGS_FILE_NAME, "tmp", - firebaseApp.getApplicationContext().getFilesDir()); - - FileOutputStream fos = new FileOutputStream(tmpFile); - fos.write(prefs.toString().getBytes()); - fos.close(); - tmpFile.renameTo(dataFile); - } - - public void writePreferencesToDisk(@NonNull PersistedInstallationEntry entryValue) { + /** + * Write the prefs to a JSON object, serialize them into a JSON string and write the bytes to a + * temp file. After writing and closing the temp file, rename it over to the actual + * SETTINGS_FILE_NAME. + */ + @NonNull + public PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry( + @NonNull PersistedInstallationEntry prefs) { try { + // Write the prefs into a JSON object JSONObject json = new JSONObject(); - json.put( - getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), - entryValue.getFirebaseInstallationId()); - json.put( - getSharedPreferencesKey(PERSISTED_STATUS_KEY), - entryValue.getRegistrationStatus().ordinal()); - json.put(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); - json.put(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); - json.put( - getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), - entryValue.getTokenCreationEpochInSecs()); - json.put( - getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); - json.put(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); - writeJSONToFile(json); + json.put(FIREBASE_INSTALLATION_ID_KEY, prefs.getFirebaseInstallationId()); + json.put(PERSISTED_STATUS_KEY, prefs.getRegistrationStatus().ordinal()); + json.put(AUTH_TOKEN_KEY, prefs.getAuthToken()); + json.put(REFRESH_TOKEN_KEY, prefs.getRefreshToken()); + json.put(TOKEN_CREATION_TIME_IN_SECONDS_KEY, prefs.getTokenCreationEpochInSecs()); + json.put(EXPIRES_IN_SECONDS_KEY, prefs.getExpiresInSecs()); + json.put(FIS_ERROR_KEY, prefs.getFisError()); + File tmpFile = + File.createTempFile( + SETTINGS_FILE_NAME, "tmp", firebaseApp.getApplicationContext().getFilesDir()); + + // Werialize the JSON object into a string and write the bytes to a temp file + FileOutputStream fos = new FileOutputStream(tmpFile); + fos.write(json.toString().getBytes("UTF-8")); + fos.close(); + + // Snapshot the temp file to the actual file + if (!tmpFile.renameTo(dataFile)) { + throw new IOException("unable to rename the tmpfile to " + SETTINGS_FILE_NAME); + } } catch (JSONException | IOException e) { - // ignore + // This should only happen when the storage is full or the system is corrupted. + // There isn't a lot we can do when this happens, other than crash the process. It is a + // bit nicer to eat the error and hope that the user clears some storage space on their + // device. } - } - /** - * Sets the state to NOT_GENERATED. - */ - public void clear() { - writePreferencesToDisk( - PersistedInstallationEntry.builder() - .setRegistrationStatus(RegistrationStatus.NOT_GENERATED) - .build()); + // Return the prefs that were written to make it easy for the caller to use them in a + // future step (e.g. for chaining calls). + return prefs; } - private String getSharedPreferencesKey(String key) { - return String.format("%s|%s", persistenceKey, key); + /** Sets the state to ATTEMPT_MIGRATION. */ + public void clearForTesting() { + dataFile.delete(); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java index aa2ffee2e58..f58f49a6afb 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; /** * This class represents a persisted fid entry in {@link PersistedInstallation}, which contains a @@ -44,6 +45,9 @@ public abstract class PersistedInstallationEntry { @Nullable public abstract String getFisError(); + @NonNull + public static PersistedInstallationEntry INSTANCE = PersistedInstallationEntry.builder().build(); + public boolean isRegistered() { return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTERED; } @@ -57,7 +61,65 @@ public boolean isUnregistered() { } public boolean isNotGenerated() { - return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED + || getRegistrationStatus() == RegistrationStatus.ATTEMPT_MIGRATION; + } + + public boolean shouldAttemptMigration() { + return getRegistrationStatus() == RegistrationStatus.ATTEMPT_MIGRATION; + } + + @NonNull + public PersistedInstallationEntry withUnregisteredFid(@NonNull String fid) { + return toBuilder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.UNREGISTERED) + .build(); + } + + @NonNull + public PersistedInstallationEntry withRegisteredFid( + @NonNull String fid, + @NonNull String refreshToken, + long creationTime, + @Nullable String authToken, + long authTokenExpiration) { + return toBuilder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setExpiresInSecs(authTokenExpiration) + .setTokenCreationEpochInSecs(creationTime) + .build(); + } + + @NonNull + public PersistedInstallationEntry withFisError(@NonNull String message) { + return toBuilder() + .setFisError(message) + .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) + .build(); + } + + @NonNull + public PersistedInstallationEntry withNoGeneratedFid() { + return toBuilder().setRegistrationStatus(RegistrationStatus.NOT_GENERATED).build(); + } + + @NonNull + public PersistedInstallationEntry withAuthToken( + @NonNull String authToken, long authTokenExpiration, long creationTime) { + return toBuilder() + .setAuthToken(authToken) + .setExpiresInSecs(authTokenExpiration) + .setTokenCreationEpochInSecs(creationTime) + .build(); + } + + @NonNull + public PersistedInstallationEntry withClearedAuthToken() { + return toBuilder().setAuthToken(null).build(); } @NonNull @@ -68,7 +130,7 @@ public boolean isNotGenerated() { public static PersistedInstallationEntry.Builder builder() { return new AutoValue_PersistedInstallationEntry.Builder() .setTokenCreationEpochInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.NOT_GENERATED) + .setRegistrationStatus(RegistrationStatus.ATTEMPT_MIGRATION) .setExpiresInSecs(0); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index f26aca7eb8d..cf55287b376 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -26,6 +26,8 @@ import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; +import com.google.firebase.installations.FirebaseInstallationsException; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import java.io.BufferedReader; import java.io.IOException; @@ -85,57 +87,58 @@ public FirebaseInstallationServiceClient(@NonNull Context context) { * @param projectID Project Id * @param appId the identifier of a Firebase application * @return {@link InstallationResponse} generated from the response body + * 400: return response with status BAD_CONFIG + * 403: return response with status BAD_CONFIG + * 403: return response with status BAD_CONFIG + * 429: throw IOException + * 500: throw IOException */ @NonNull public InstallationResponse createFirebaseInstallation( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) - throws FirebaseException { + throws IOException { String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); - try { - int retryCount = 0; - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - while (retryCount <= MAX_RETRIES) { - HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.setDoOutput(true); - - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } + int retryCount = 0; + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.setDoOutput(true); + + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } - int httpResponseCode = httpsURLConnection.getResponseCode(); + int httpResponseCode = httpsURLConnection.getResponseCode(); - if (httpResponseCode == 200) { - return readCreateResponse(httpsURLConnection); - } - // Usually the FIS server recovers from errors: retry one time before giving up. - if (httpResponseCode >= 500 && httpResponseCode < 600) { - retryCount++; - continue; - } + if (httpResponseCode == 200) { + return readCreateResponse(httpsURLConnection); + } - // Unrecoverable server response or unknown error - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; } - // Return empty installation response with SERVER_ERROR response code after max retries - return InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + // Return empty installation response with BAD_CONFIG response code after max retries + return InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build(); } + + throw new IOException(); } private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, String appId) @@ -162,32 +165,39 @@ public void deleteFirebaseInstallation( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseException { + throws FirebaseException, IOException { String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); - try { - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + int retryCount = 0; + while (retryCount <= MAX_RETRIES) { HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); httpsURLConnection.setRequestMethod("DELETE"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return; - default: - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + + if (httpResponseCode == 200 || httpResponseCode == 401 || httpResponseCode == 404) { + return; + } + + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; } - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + throw new FirebaseInstallationsException("bad config while trying to delete FID", + Status.BAD_CONFIG); } + + throw new IOException(); } /** @@ -198,6 +208,12 @@ public void deleteFirebaseInstallation( * @param fid Firebase Installation Identifier * @param projectID Project Id * @param refreshToken a token used to authenticate FIS requests + * 400: return response with status BAD_CONFIG + * 401: return response with status INVALID_AUTH + * 403: return response with status BAD_CONFIG + * 404: return response with status INVALID_AUTH + * 429: throw IOException + * 500: throw IOException */ @NonNull public TokenResult generateAuthToken( @@ -205,53 +221,42 @@ public TokenResult generateAuthToken( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseException { + throws IOException { String resourceName = String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); - try { - int retryCount = 0; - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - while (retryCount <= MAX_RETRIES) { - HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - - int httpResponseCode = httpsURLConnection.getResponseCode(); - - if (httpResponseCode == 200) { - return readGenerateAuthTokenResponse(httpsURLConnection); - } + int retryCount = 0; + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - if (httpResponseCode == 401) { - return TokenResult.builder() - .setResponseCode(TokenResult.ResponseCode.REFRESH_TOKEN_ERROR) - .build(); - } + int httpResponseCode = httpsURLConnection.getResponseCode(); - if (httpResponseCode == 404) { - return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build(); - } + if (httpResponseCode == 200) { + return readGenerateAuthTokenResponse(httpsURLConnection); + } - // Usually the FIS server recovers from errors: retry one time before giving up. - if (httpResponseCode >= 500 && httpResponseCode < 600) { - retryCount++; - continue; - } + if (httpResponseCode == 401 || httpResponseCode == 404) { + return TokenResult.builder() + .setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build(); + } - // Unrecoverable server response or unknown error - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; } - throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build(); } + throw new IOException(); } private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index 0c0463b6f49..213b4d19416 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -24,8 +24,9 @@ public abstract class InstallationResponse { public enum ResponseCode { // Returned on success OK, - // An error occurred on the server while processing this request(temporary) - SERVER_ERROR + // The request is invalid. Do not try again without fixing the request. Usually means + // a bad or misconfigured API Key or project is being used. + BAD_CONFIG, } @Nullable diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java index a120e418ff0..1fd7a5d99fd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java @@ -27,14 +27,10 @@ public enum ResponseCode { OK, // Auth token cannot be generated for this FID in the request. Because it is not // registered/found on the FIS server. Recreate a new fid to fetch a valid auth token. - FID_ERROR, + BAD_CONFIG, // Refresh token in this request in not accepted by the FIS server. Either it has been blocked // or changed. Recreate a new fid to fetch a valid auth token. - REFRESH_TOKEN_ERROR, - } - - public boolean isSuccessful() { - return getResponseCode() == ResponseCode.OK; + AUTH_ERROR, } /** A new FIS Auth-Token, created for this Firebase Installation. */ From 804fafe1d59713cdf1a71cb6485fe89c210b70c9 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Thu, 5 Dec 2019 14:54:18 -0800 Subject: [PATCH 3/7] revert the name of the FID prefs write call --- firebase-installations/api.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 5dcee6cf73f..e7ba50cd749 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -38,8 +38,8 @@ package com.google.firebase.installations.local { public class PersistedInstallation { ctor public PersistedInstallation(@NonNull FirebaseApp); method public void clearForTesting(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry readPersistedInstallationEntryValue(); - method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry writePreferencesToDisk(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); } public enum PersistedInstallation.RegistrationStatus { From f03ebc96cd4ee0c0d95311a976983bd7210023fa Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Thu, 5 Dec 2019 15:04:23 -0800 Subject: [PATCH 4/7] fix formatting issues --- .../firebase/installations/FakeCalendar.java | 16 +- ...FirebaseInstallationsInstrumentedTest.java | 159 +++++++++--------- .../FisAndroidTestConstants.java | 1 - .../installations/FirebaseInstallations.java | 63 ++++--- .../FirebaseInstallationsException.java | 4 +- .../installations/GetAuthTokenListener.java | 3 +- .../installations/RandomFidGenerator.java | 12 +- .../FirebaseInstallationServiceClient.java | 37 ++-- 8 files changed, 145 insertions(+), 150 deletions(-) diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java index e7802d29bf9..1775a7e8cff 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java @@ -36,24 +36,16 @@ public void advanceTimeBySeconds(long deltaSeconds) { } @Override - protected void computeTime() { - - } + protected void computeTime() {} @Override - protected void computeFields() { - - } + protected void computeFields() {} @Override - public void add(int i, int i1) { - - } + public void add(int i, int i1) {} @Override - public void roll(int i, boolean b) { - - } + public void roll(int i, boolean b) {} @Override public int getMinimum(int i) { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 2991fccd1c4..de6677a40ef 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -148,17 +148,18 @@ public void cleanUp() { /** * Check the id generation process when there is no network. There are three cases: - * - no iid -> generate a new fid - * - iid present -> make that iid into a fid - * - fid generated -> return that fid + * + *

    + *
  • no iid -> generate a new fid + *
  • iid present -> make that iid into a fid + *
  • fid generated -> return that fid + *
*/ @Test public void testGetId_noNetwork_noIid() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); when(mockIidStore.readIid()).thenReturn(null); @@ -185,11 +186,9 @@ public void testGetId_noNetwork_noIid() throws Exception { @Test public void testGetId_noNetwork_iidPresent() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); @@ -216,15 +215,13 @@ public void testGetId_noNetwork_iidPresent() throws Exception { @Test public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withUnregisteredFid("generatedFid")); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); // Do the actual getId() call under test. Confirm that it returns the already generated FID. // Confirm both that it returns the expected ID, as does reading the prefs from storage. @@ -248,8 +245,8 @@ public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { */ @Test public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withRegisteredFid( + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( TEST_FID_1, TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), @@ -277,16 +274,16 @@ public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { } /** - * Checks that if we have an unregistered fid that the fid gets registered with the backend - * and no other calls are made. + * Checks that if we have an unregistered fid that the fid gets registered with the backend and no + * other calls are made. */ @Test public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { - when(mockBackend - .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), matches(TEST_FID_1), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withUnregisteredFid(TEST_FID_1)); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); // No exception, means success. assertWithMessage("getId Task failed.") @@ -314,8 +311,7 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); // Do the actual getId() call under test. @@ -341,8 +337,7 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { when(mockIidStore.readIid()).thenReturn(null); - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); // Call getId multiple times @@ -367,16 +362,15 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { } /** - * Checks that if the server rejects a FID during registration the SDK will use the - * fid in the response as the new fid. + * Checks that if the server rejects a FID during registration the SDK will use the fid in the + * response as the new fid. */ @Test public void testGetId_unregistered_replacesFidWithResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); - when(mockBackend - .createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); // The first call will return the existing FID, "tobereplaced" @@ -394,14 +388,14 @@ public void testGetId_unregistered_replacesFidWithResponse() throws Exception { } /** - * A registration that fails with a SERVER_ERROR will cause the FID to be put into the - * error state. + * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error + * state. */ @Test public void testGetId_ServerError_UnregisteredFID() throws Exception { // start with an unregistered fid - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withUnregisteredFid(TEST_FID_1)); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); // have the server return a server error for the registration when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) @@ -425,8 +419,8 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception { } /** - * A registration that fails with an IOException will not cause the FID to be put into the - * error state. + * A registration that fails with an IOException will not cause the FID to be put into the error + * state. */ @Test public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { @@ -457,9 +451,11 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws // Start with a registered FID persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // Move the time forward by the token expiration time. fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); @@ -483,21 +479,22 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws } /** - * The FID is successfully registered but the token is expired. A getId will cause the token - * to be refreshed in the background. + * The FID is successfully registered but the token is expired. A getId will cause the token to be + * refreshed in the background. */ @Test public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { // Start with a registered FID persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_TOKEN_RESULT); // Move the time forward by the token expiration time. @@ -514,8 +511,10 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { // Check that the token has been refreshed assertWithMessage("auth token is not what is expected after the refresh") - .that(Tasks.await(firebaseInstallations.getToken( - FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)).getToken()) + .that( + Tasks.await( + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)) + .getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); verify(mockBackend, never()) @@ -526,8 +525,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -540,9 +538,11 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { public void testGetAuthToken_fidExists_successful() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); InstallationTokenResult installationTokenResult = Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -578,8 +578,8 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { // Update local storage with a unregistered installation entry to validate that getToken // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withUnregisteredFid(TEST_FID_1)); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); @@ -593,13 +593,16 @@ public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Excepti @Test public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(PersistedInstallationEntry.INSTANCE - .withRegisteredFid(TEST_FID_1, TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // Mocks error during auth token generation - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenReturn( TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); @@ -626,21 +629,23 @@ public void testGetAuthToken_serverError_failure() throws Exception { // start the test with a registered FID persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // have the backend fail when generateAuthToken is invoked. - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(TokenResult.builder() - .setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); // Make the forced getAuthToken call, which should fail. try { Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " - + "returned by the network call."); + fail( + "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " + + "returned by the network call."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -658,13 +663,14 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( // start with a valid fid and authtoken persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_TOKEN_RESULT); // expire the authtoken by advancing the clock @@ -693,9 +699,11 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th // start with a valid fid and authtoken persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, TEST_REFRESH_TOKEN, + TEST_FID_1, + TEST_REFRESH_TOKEN, utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getToken() @@ -747,8 +755,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - when(mockBackend.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.delete()); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 2080a55aff8..769ea8ec5a9 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -77,5 +77,4 @@ public final class FisAndroidTestConstants { .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setResponseCode(TokenResult.ResponseCode.OK) .build(); - } 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 6ce96846863..4140af81044 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 @@ -24,20 +24,20 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; -import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.heartbeatinfo.HeartBeatInfo; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.TokenResult; +import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; -import com.google.firebase.platforminfo.UserAgentPublisher; import java.util.ArrayList; import java.util.Calendar; import java.util.Iterator; @@ -247,13 +247,12 @@ private final void doGetAuthTokenForceRefresh() { /** * Logic for handling get id and the two forms of get auth token. This handles all the work, - * including creating a new FID if one hasn't been generated yet and making the network - * calls to create an installation and to retrieve a new auth token. Also contains the - * error handling for when the server says that credentials are bad and that a new Fid - * needs to be generated. - * @param forceRefresh true if this is for a getAuthToken call and if the caller wants - * to fetch a new auth token from the server even if an unexpired auth token exists - * on the client. + * including creating a new FID if one hasn't been generated yet and making the network calls to + * create an installation and to retrieve a new auth token. Also contains the error handling for + * when the server says that credentials are bad and that a new Fid needs to be generated. + * + * @param forceRefresh true if this is for a getAuthToken call and if the caller wants to fetch a + * new auth token from the server even if an unexpired auth token exists on the client. */ private final void doRegistrationInternal(boolean forceRefresh) { PersistedInstallationEntry prefs = persistedInstallation.readPersistedInstallationEntryValue(); @@ -306,17 +305,17 @@ private final void doRegistrationInternal(boolean forceRefresh) { } /** - * Generate a new FID, either from an existing IID or generated randomly. - * If an IID exists and this is the first time a FID has been generated for this - * installation, the IID will be used as the FID. If the FID is ever cleared then - * the next time a FID is generated the IID is ignored and a FID is generated - * randomly. - *

- * This method ensures that only one thread in one process will run this code at a - * time, so that two different processes or threads don't create two different FIDs. + * Generate a new FID, either from an existing IID or generated randomly. If an IID exists and + * this is the first time a FID has been generated for this installation, the IID will be used as + * the FID. If the FID is ever cleared then the next time a FID is generated the IID is ignored + * and a FID is generated randomly. + * + *

This method ensures that only one thread in one process will run this code at a time, so + * that two different processes or threads don't create two different FIDs. + * * @param prefs takes the current state of the prefs * @return a new version of the prefs that includes the new FID. These prefs will have already - * been persisted. + * been persisted. */ private PersistedInstallationEntry generateAndPersistFidProcessSafe( PersistedInstallationEntry prefs) { @@ -326,22 +325,21 @@ private PersistedInstallationEntry generateAndPersistFidProcessSafe( // Only one single thread from one single process can execute this block // at any given time. String fid = readExistingIidOrCreateFid(prefs); - prefs = persistedInstallation.insertOrUpdatePersistedInstallationEntry( - prefs.withUnregisteredFid(fid)); + prefs = + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + prefs.withUnregisteredFid(fid)); return prefs; } } finally { - releaseCrossProcessLock(fileLock); + releaseCrossProcessLock(fileLock); } } - /** - * Use file locking to acquire a lock that will also block other processes. - */ + /** Use file locking to acquire a lock that will also block other processes. */ private FileLock getCrossProcessLock() { try { - File file = new File(firebaseApp.getApplicationContext().getFilesDir(), - LOCKFILE_NAME_GENERATE_FID); + File file = + new File(firebaseApp.getApplicationContext().getFilesDir(), LOCKFILE_NAME_GENERATE_FID); FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); // Use the file channel to create a lock on the file. // This method blocks until it can retrieve the lock. @@ -351,9 +349,7 @@ private FileLock getCrossProcessLock() { } } - /** - * Release a previously acquired lock. - */ + /** Release a previously acquired lock. */ private void releaseCrossProcessLock(FileLock fileLock) { try { fileLock.release(); @@ -385,7 +381,7 @@ private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEn /*projectID= */ firebaseApp.getOptions().getProjectId(), /*appId= */ getApplicationId()); - switch(response.getResponseCode()) { + switch (response.getResponseCode()) { case OK: return prefs.withRegisteredFid( response.getFid(), @@ -394,7 +390,7 @@ private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEn response.getAuthToken().getToken(), response.getAuthToken().getTokenExpirationTimestamp()); case BAD_CONFIG: - return prefs.withFisError("BAD CONFIG"); + return prefs.withFisError("BAD CONFIG"); default: throw new IOException(); } @@ -415,7 +411,7 @@ private PersistedInstallationEntry fetchAuthTokenFromServer( /*projectID= */ firebaseApp.getOptions().getProjectId(), /*refreshToken= */ prefs.getRefreshToken()); - switch(tokenResult.getResponseCode()) { + switch (tokenResult.getResponseCode()) { case OK: return prefs.withAuthToken( tokenResult.getToken(), @@ -449,8 +445,7 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio } catch (FirebaseException exception) { throw new FirebaseInstallationsException( - "Failed to delete a Firebase Installation.", - Status.BAD_CONFIG); + "Failed to delete a Firebase Installation.", Status.BAD_CONFIG); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index ad82366ed0d..0918d059f56 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -23,8 +23,8 @@ public enum Status { OK, /** - * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key - * or Project. + * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key or + * Project. */ BAD_CONFIG, } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index 52dd3c46989..2c067a30507 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -22,8 +22,7 @@ class GetAuthTokenListener implements StateListener { private final TaskCompletionSource resultTaskCompletionSource; public GetAuthTokenListener( - Utils utils, - TaskCompletionSource resultTaskCompletionSource) { + Utils utils, TaskCompletionSource resultTaskCompletionSource) { this.utils = utils; this.resultTaskCompletionSource = resultTaskCompletionSource; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java index b06fbe6fb5e..9931d20045c 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java @@ -66,12 +66,12 @@ public String createRandomFid() { */ private static String encodeFidBase64UrlSafe(byte[] rawValue) { return new String( - android.util.Base64.encode( - rawValue, - android.util.Base64.URL_SAFE - | android.util.Base64.NO_PADDING - | android.util.Base64.NO_WRAP), - Charset.defaultCharset()) + android.util.Base64.encode( + rawValue, + android.util.Base64.URL_SAFE + | android.util.Base64.NO_PADDING + | android.util.Base64.NO_WRAP), + Charset.defaultCharset()) .substring(0, FID_LENGTH); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 12d7cc3b192..bb392f9472e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -27,10 +27,10 @@ import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; -import com.google.firebase.installations.FirebaseInstallationsException; -import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.heartbeatinfo.HeartBeatInfo; import com.google.firebase.heartbeatinfo.HeartBeatInfo.HeartBeat; +import com.google.firebase.installations.FirebaseInstallationsException; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.BufferedReader; @@ -101,11 +101,13 @@ public FirebaseInstallationServiceClient( * @param projectID Project Id * @param appId the identifier of a Firebase application * @return {@link InstallationResponse} generated from the response body - * 400: return response with status BAD_CONFIG - * 403: return response with status BAD_CONFIG - * 403: return response with status BAD_CONFIG - * 429: throw IOException - * 500: throw IOException + *

    + *
  • 400: return response with status BAD_CONFIG + *
  • 403: return response with status BAD_CONFIG + *
  • 403: return response with status BAD_CONFIG + *
  • 429: throw IOException + *
  • 500: throw IOException + *
*/ @NonNull public InstallationResponse createFirebaseInstallation( @@ -207,8 +209,8 @@ public void deleteFirebaseInstallation( continue; } - throw new FirebaseInstallationsException("bad config while trying to delete FID", - Status.BAD_CONFIG); + throw new FirebaseInstallationsException( + "bad config while trying to delete FID", Status.BAD_CONFIG); } throw new IOException(); @@ -222,12 +224,14 @@ public void deleteFirebaseInstallation( * @param fid Firebase Installation Identifier * @param projectID Project Id * @param refreshToken a token used to authenticate FIS requests - * 400: return response with status BAD_CONFIG - * 401: return response with status INVALID_AUTH - * 403: return response with status BAD_CONFIG - * 404: return response with status INVALID_AUTH - * 429: throw IOException - * 500: throw IOException + *
    + *
  • 400: return response with status BAD_CONFIG + *
  • 401: return response with status INVALID_AUTH + *
  • 403: return response with status BAD_CONFIG + *
  • 404: return response with status INVALID_AUTH + *
  • 429: throw IOException + *
  • 500: throw IOException + *
*/ @NonNull public TokenResult generateAuthToken( @@ -259,8 +263,7 @@ public TokenResult generateAuthToken( } if (httpResponseCode == 401 || httpResponseCode == 404) { - return TokenResult.builder() - .setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build(); + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build(); } if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { From 1769b093df7c2e48af0020687e7b91dad58588e1 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Fri, 6 Dec 2019 16:48:57 -0800 Subject: [PATCH 5/7] Fix the cross process locking The check if a new FID was needed was not in the critical region. --- .../installations/FirebaseInstallations.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) 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 4140af81044..9ebab2daacf 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 @@ -255,14 +255,7 @@ private final void doGetAuthTokenForceRefresh() { * new auth token from the server even if an unexpired auth token exists on the client. */ private final void doRegistrationInternal(boolean forceRefresh) { - PersistedInstallationEntry prefs = persistedInstallation.readPersistedInstallationEntryValue(); - - // Check if a new FID needs to be created - if (prefs.isNotGenerated()) { - // For a default firebase installation read the existing iid. For other custom firebase - // installations create a new fid - prefs = generateAndPersistFidProcessSafe(prefs); - } + PersistedInstallationEntry prefs = getPrefsWithGeneratedIdMultiProcessSafe(); // Since the caller wants to force an authtoken refresh remove the authtoken from the // prefs we are working with, so the following steps know a new token is required. @@ -305,31 +298,38 @@ private final void doRegistrationInternal(boolean forceRefresh) { } /** - * Generate a new FID, either from an existing IID or generated randomly. If an IID exists and - * this is the first time a FID has been generated for this installation, the IID will be used as - * the FID. If the FID is ever cleared then the next time a FID is generated the IID is ignored - * and a FID is generated randomly. - * - *

This method ensures that only one thread in one process will run this code at a time, so - * that two different processes or threads don't create two different FIDs. + * Loads the prefs, generating a new ID if necessary. 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 cross-process lock created using FileLocks. + *

+ * If a FID does not yet exist it generate a new FID, either from an existing IID or generated + * randomly. If an IID exists and this is the first time a FID has been generated for this + * installation, the IID will be used as the FID. If the FID is ever cleared then the next + * time a FID is generated the IID is ignored and a FID is generated randomly. * - * @param prefs takes the current state of the prefs * @return a new version of the prefs that includes the new FID. These prefs will have already * been persisted. */ - private PersistedInstallationEntry generateAndPersistFidProcessSafe( - PersistedInstallationEntry prefs) { + private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { FileLock fileLock = getCrossProcessLock(); try { synchronized (lockGenerateFid) { - // Only one single thread from one single process can execute this block - // at any given time. - String fid = readExistingIidOrCreateFid(prefs); - prefs = - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - prefs.withUnregisteredFid(fid)); + PersistedInstallationEntry prefs = + persistedInstallation.readPersistedInstallationEntryValue(); + // Check if a new FID needs to be created + if (prefs.isNotGenerated()) { + // For a default firebase installation read the existing iid. For other custom firebase + // installations create a new fid + + // Only one single thread from one single process can execute this block + // at any given time. + String fid = readExistingIidOrCreateFid(prefs); + prefs = persistedInstallation + .insertOrUpdatePersistedInstallationEntry(prefs.withUnregisteredFid(fid)); + } return prefs; } + } finally { releaseCrossProcessLock(fileLock); } From 27ecdcca1aff0bf89a2cb27d20122aa96d174c78 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Mon, 9 Dec 2019 15:44:57 -0800 Subject: [PATCH 6/7] some small changes from the review --- .../firebase/installations/FakeCalendar.java | 4 ++-- ...FirebaseInstallationsInstrumentedTest.java | 2 +- .../local/PersistedInstallationTest.java | 4 ++-- .../FirebaseInstallationsException.java | 2 -- .../local/PersistedInstallation.java | 19 +++++++------------ 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java index 1775a7e8cff..3190d5911e8 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java @@ -19,8 +19,8 @@ public class FakeCalendar extends Calendar { private long timeInMillis; - public FakeCalendar() { - timeInMillis = 5000000; + public FakeCalendar(long initialTimeInMillis) { + timeInMillis = initialTimeInMillis; } public long getTimeInMillis() { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index de6677a40ef..4fec8c4cce5 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -110,7 +110,7 @@ public void setUp() throws FirebaseException, IOException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); - fakeCalendar = new FakeCalendar(); + fakeCalendar = new FakeCalendar(5000000L); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java index 53f39fe3a58..495dcd38991 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java @@ -76,7 +76,7 @@ public void testReadPersistedInstallationEntry_Null() { @Test public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exception { - // Insert Persisted Installation Entry with Unregistered status in Shared Prefs + // Write the Persisted Installation Entry with Unregistered status to storage. persistedInstallation0.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) @@ -97,7 +97,7 @@ public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exce assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_1); - // Update Persisted Fid Entry with Registered status in Shared Prefs + // Write the Persisted Fid Entry with Registered status to storage. persistedInstallation0.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index 0918d059f56..07683203570 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -20,8 +20,6 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ public class FirebaseInstallationsException extends FirebaseException { public enum Status { - OK, - /** * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key or * Project. diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index ac4ac34facd..f40377287dc 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -33,8 +33,8 @@ public class PersistedInstallation { @NonNull private final FirebaseApp firebaseApp; // Registration Status of each persisted fid entry - // NOTE: never change the ordinal of the enum values because the enum values are stored in shared - // prefs as their ordinal numbers. + // NOTE: never change the ordinal of the enum values because the enum values are written to + // local storage as their ordinal numbers. public enum RegistrationStatus { /** * {@link PersistedInstallationEntry} legacy registration status. Next state: UNREGISTERED - A @@ -65,8 +65,7 @@ public enum RegistrationStatus { REGISTER_ERROR, } - private static final String SETTINGS_FILE_NAME = "PersistedInstallation"; - + private static final String SETTINGS_FILE_NAME_PREFIX = "PersistedInstallation"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; private static final String REFRESH_TOKEN_KEY = "RefreshToken"; @@ -75,16 +74,13 @@ public enum RegistrationStatus { private static final String PERSISTED_STATUS_KEY = "Status"; private static final String FIS_ERROR_KEY = "FisError"; - private final String persistenceKey; - public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path - persistenceKey = firebaseApp.getPersistenceKey(); dataFile = new File( firebaseApp.getApplicationContext().getFilesDir(), - SETTINGS_FILE_NAME + "." + persistenceKey + ".json"); + SETTINGS_FILE_NAME_PREFIX + "." + firebaseApp.getPersistenceKey() + ".json"); this.firebaseApp = firebaseApp; } @@ -148,9 +144,8 @@ public PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry( json.put(TOKEN_CREATION_TIME_IN_SECONDS_KEY, prefs.getTokenCreationEpochInSecs()); json.put(EXPIRES_IN_SECONDS_KEY, prefs.getExpiresInSecs()); json.put(FIS_ERROR_KEY, prefs.getFisError()); - File tmpFile = - File.createTempFile( - SETTINGS_FILE_NAME, "tmp", firebaseApp.getApplicationContext().getFilesDir()); + File tmpFile = File.createTempFile(SETTINGS_FILE_NAME_PREFIX, + "tmp", firebaseApp.getApplicationContext().getFilesDir()); // Werialize the JSON object into a string and write the bytes to a temp file FileOutputStream fos = new FileOutputStream(tmpFile); @@ -159,7 +154,7 @@ public PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry( // Snapshot the temp file to the actual file if (!tmpFile.renameTo(dataFile)) { - throw new IOException("unable to rename the tmpfile to " + SETTINGS_FILE_NAME); + throw new IOException("unable to rename the tmpfile to " + SETTINGS_FILE_NAME_PREFIX); } } catch (JSONException | IOException e) { // This should only happen when the storage is full or the system is corrupted. From a9b109a7a0d21794660d62395cf593bb7371a747 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Mon, 9 Dec 2019 17:03:47 -0800 Subject: [PATCH 7/7] fix the java format and update the api file --- firebase-installations/api.txt | 1 - .../installations/FirebaseInstallations.java | 13 +++++++------ .../installations/local/PersistedInstallation.java | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index e7ba50cd749..b4fd32edc8a 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -18,7 +18,6 @@ package com.google.firebase.installations { public enum FirebaseInstallationsException.Status { enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status BAD_CONFIG; - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status OK; } public class RandomFidGenerator { 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 9ebab2daacf..3dc316a7b8e 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 @@ -301,11 +301,11 @@ private final void doRegistrationInternal(boolean forceRefresh) { * Loads the prefs, generating a new ID if necessary. 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 cross-process lock created using FileLocks. - *

- * If a FID does not yet exist it generate a new FID, either from an existing IID or generated + * + *

If a FID does not yet exist it generate a new FID, either from an existing IID or generated * randomly. If an IID exists and this is the first time a FID has been generated for this - * installation, the IID will be used as the FID. If the FID is ever cleared then the next - * time a FID is generated the IID is ignored and a FID is generated randomly. + * installation, the IID will be used as the FID. If the FID is ever cleared then the next time a + * FID is generated the IID is ignored and a FID is generated randomly. * * @return a new version of the prefs that includes the new FID. These prefs will have already * been persisted. @@ -324,8 +324,9 @@ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { // Only one single thread from one single process can execute this block // at any given time. String fid = readExistingIidOrCreateFid(prefs); - prefs = persistedInstallation - .insertOrUpdatePersistedInstallationEntry(prefs.withUnregisteredFid(fid)); + prefs = + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + prefs.withUnregisteredFid(fid)); } return prefs; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index f40377287dc..d3786c4b6e4 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -144,8 +144,9 @@ public PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry( json.put(TOKEN_CREATION_TIME_IN_SECONDS_KEY, prefs.getTokenCreationEpochInSecs()); json.put(EXPIRES_IN_SECONDS_KEY, prefs.getExpiresInSecs()); json.put(FIS_ERROR_KEY, prefs.getFisError()); - File tmpFile = File.createTempFile(SETTINGS_FILE_NAME_PREFIX, - "tmp", firebaseApp.getApplicationContext().getFilesDir()); + File tmpFile = + File.createTempFile( + SETTINGS_FILE_NAME_PREFIX, "tmp", firebaseApp.getApplicationContext().getFilesDir()); // Werialize the JSON object into a string and write the bytes to a temp file FileOutputStream fos = new FileOutputStream(tmpFile);