diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt
index e2120cdf91f..2024c519fd0 100644
--- a/firebase-installations/api.txt
+++ b/firebase-installations/api.txt
@@ -32,6 +32,7 @@ package com.google.firebase.installations.local {
public class IidStore {
ctor public IidStore();
method @Nullable public String readIid();
+ method @Nullable public String readToken();
}
public class PersistedInstallation {
@@ -92,7 +93,7 @@ package com.google.firebase.installations.remote {
public class FirebaseInstallationServiceClient {
ctor public FirebaseInstallationServiceClient(@NonNull android.content.Context, @Nullable com.google.firebase.platforminfo.UserAgentPublisher, @Nullable com.google.firebase.heartbeatinfo.HeartBeatInfo);
- method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException;
+ method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @Nullable String) throws java.io.IOException;
method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.FirebaseException, java.io.IOException;
method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException;
}
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 7abd5351ed7..8288d2c205f 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
@@ -307,8 +307,8 @@ private final void doRegistrationInternal(boolean forceRefresh) {
* been persisted.
*/
private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() {
- CrossProcessLock lock = CrossProcessLock
- .acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID);
+ CrossProcessLock lock =
+ CrossProcessLock.acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID);
try {
synchronized (lockGenerateFid) {
PersistedInstallationEntry prefs =
@@ -354,7 +354,8 @@ private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEn
/*apiKey= */ firebaseApp.getOptions().getApiKey(),
/*fid= */ prefs.getFirebaseInstallationId(),
/*projectID= */ firebaseApp.getOptions().getProjectId(),
- /*appId= */ getApplicationId());
+ /*appId= */ getApplicationId(),
+ /* migration-header= */ null);
switch (response.getResponseCode()) {
case OK:
diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java
index 4e8366df7bf..704ef19f3b6 100644
--- a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java
@@ -30,6 +30,8 @@
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
+import org.json.JSONException;
+import org.json.JSONObject;
/**
* Read existing iid only for default (first initialized) instance of this firebase application.*
@@ -38,10 +40,16 @@ public class IidStore {
private static final String IID_SHARED_PREFS_NAME = "com.google.android.gms.appid";
private static final String STORE_KEY_PUB = "|S||P|";
private static final String STORE_KEY_ID = "|S|id";
+ private static final String STORE_KEY_TOKEN = "|T|";
+ private static final String DEFAULT_SCOPE = "|*";
+ private static final String JSON_TOKEN_KEY = "token";
+ private static final String JSON_ENCODED_PREFIX = "{";
@GuardedBy("iidPrefs")
private final SharedPreferences iidPrefs;
+ private final String defaultSenderId;
+
public IidStore() {
// Different FirebaseApp in the same Android application should have the same application
// context and same dir path. We only read existing Iids for the default firebase application.
@@ -49,6 +57,62 @@ public IidStore() {
FirebaseApp.getInstance()
.getApplicationContext()
.getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+
+ defaultSenderId = getDefaultSenderId(FirebaseApp.getInstance());
+ }
+
+ private static String getDefaultSenderId(FirebaseApp app) {
+ // Check for an explicit sender id
+ String senderId = app.getOptions().getGcmSenderId();
+ if (senderId != null) {
+ return senderId;
+ }
+ String appId = app.getOptions().getApplicationId();
+ if (!appId.startsWith("1:") && !appId.startsWith("2:")) {
+ // If applicationId does not contain a (GMP-)App-ID, it contains a Sender identifier
+ return appId;
+ }
+ // For v1 app IDs, fall back to parsing the project number out
+ @SuppressWarnings("StringSplitter")
+ String[] parts = appId.split(":");
+ if (parts.length != 4) {
+ return null; // Invalid format
+ }
+ String projectNumber = parts[1];
+ if (projectNumber.isEmpty()) {
+ return null; // No project number
+ }
+ return projectNumber;
+ }
+
+ private String createTokenKey(@NonNull String senderId) {
+ return STORE_KEY_TOKEN + senderId + DEFAULT_SCOPE;
+ }
+
+ @Nullable
+ public String readToken() {
+ synchronized (iidPrefs) {
+ String token = iidPrefs.getString(createTokenKey(defaultSenderId), null);
+ if (token.isEmpty()) {
+ return null;
+ }
+
+ if (token.startsWith(JSON_ENCODED_PREFIX)) {
+ return parseIidTokenFromJson(token);
+ }
+ // Legacy value, token is whole string
+ return token;
+ }
+ }
+
+ private String parseIidTokenFromJson(String token) {
+ // Encoded as JSON
+ try {
+ JSONObject json = new JSONObject(token);
+ return json.getString(JSON_TOKEN_KEY);
+ } catch (JSONException e) {
+ return null;
+ }
}
@Nullable
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 bb392f9472e..a92dd0ba922 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
@@ -70,6 +70,8 @@ public class FirebaseInstallationServiceClient {
private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package";
private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert";
+ private static final String X_ANDROID_IID_MIGRATION_KEY = "x-goog-fis-android-iid-migration-auth";
+
private static final int NETWORK_TIMEOUT_MILLIS = 10000;
private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s");
@@ -87,10 +89,10 @@ public class FirebaseInstallationServiceClient {
public FirebaseInstallationServiceClient(
@NonNull Context context,
@Nullable UserAgentPublisher publisher,
- @Nullable HeartBeatInfo heartBeatInfo) {
+ @Nullable HeartBeatInfo heartbeatInfo) {
this.context = context;
this.userAgentPublisher = publisher;
- this.heartbeatInfo = heartBeatInfo;
+ this.heartbeatInfo = heartbeatInfo;
}
/**
@@ -100,6 +102,8 @@ public FirebaseInstallationServiceClient(
* @param fid Firebase Installation Identifier
* @param projectID Project Id
* @param appId the identifier of a Firebase application
+ * @param iidToken the identifier token of a Firebase application with instance id. It is set to
+ * null for a FID.
* @return {@link InstallationResponse} generated from the response body
*
* - 400: return response with status BAD_CONFIG
@@ -111,7 +115,11 @@ public FirebaseInstallationServiceClient(
*/
@NonNull
public InstallationResponse createFirebaseInstallation(
- @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId)
+ @NonNull String apiKey,
+ @NonNull String fid,
+ @NonNull String projectID,
+ @NonNull String appId,
+ @Nullable String iidToken)
throws IOException {
String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID);
int retryCount = 0;
@@ -128,6 +136,11 @@ public InstallationResponse createFirebaseInstallation(
httpsURLConnection.setRequestMethod("POST");
httpsURLConnection.setDoOutput(true);
+ // Note: Set the iid token header for authenticating the Instance-ID migrating to FIS.
+ if (iidToken != null) {
+ httpsURLConnection.addRequestProperty(X_ANDROID_IID_MIGRATION_KEY, iidToken);
+ }
+
GZIPOutputStream gzipOutputStream =
new GZIPOutputStream(httpsURLConnection.getOutputStream());
try {
diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java
index 5dc10b77fda..9cabfa99494 100644
--- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java
+++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertThat;
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;
@@ -178,7 +179,8 @@ public void cleanUp() {
*/
@Test
public void testGetId_noNetwork_noIid() throws Exception {
- when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenThrow(new IOException());
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
.thenThrow(new IOException());
@@ -207,7 +209,8 @@ 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(), any()))
.thenThrow(new IOException());
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
.thenThrow(new IOException());
@@ -236,7 +239,8 @@ 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(), any()))
.thenThrow(new IOException());
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
.thenThrow(new IOException());
@@ -303,7 +307,7 @@ public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception {
@Test
public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception {
when(mockBackend.createFirebaseInstallation(
- anyString(), matches(TEST_FID_1), anyString(), anyString()))
+ anyString(), matches(TEST_FID_1), anyString(), anyString(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
@@ -322,7 +326,8 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception {
// 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());
+ .createFirebaseInstallation(
+ anyString(), matches(TEST_FID_1), anyString(), anyString(), any());
verify(mockBackend, never())
.generateAuthToken(anyString(), anyString(), anyString(), anyString());
@@ -335,7 +340,8 @@ 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(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID);
// Do the actual getId() call under test.
@@ -361,7 +367,8 @@ 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(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
// Call getId multiple times
@@ -384,7 +391,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception {
.that(task2.getResult())
.isEqualTo(TEST_FID_1);
verify(mockBackend, times(1))
- .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1);
+ .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null);
PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue();
assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1));
assertTrue("the entry isn't doesn't have a registered fid: " + entry, entry.isRegistered());
@@ -399,7 +406,8 @@ 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(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
// The first call will return the existing FID, "tobereplaced"
@@ -435,7 +443,8 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception {
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
// have the server return a server error for the registration
- when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn(
InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build());
@@ -469,7 +478,8 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
// Mocking unchecked exception on FIS createFirebaseInstallation
- when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenThrow(new IOException());
TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>();
@@ -569,14 +579,15 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
.isEqualTo(TEST_AUTH_TOKEN_2);
verify(mockBackend, never())
- .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1);
+ .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null);
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(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
TestOnCompleteListener onCompleteListener =
@@ -650,7 +661,8 @@ public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Excepti
// calls getId to ensure FID registration and returns a valid auth token.
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
- when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
TestOnCompleteListener onCompleteListener =
@@ -859,7 +871,8 @@ public void testDelete_registeredFID_successful() throws Exception {
utils.currentTimeInSecs(),
TEST_AUTH_TOKEN,
TEST_TOKEN_EXPIRATION_TIMESTAMP));
- when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
+ when(mockBackend.createFirebaseInstallation(
+ anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn(TEST_INSTALLATION_RESPONSE);
TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>();