Firebase Installations does + * + *
Should only be called if the Firebase Installation is registered.
+ *
+ * @param authTokenOption Options to get FIS Auth Token either by force refreshing or not. Accepts
+ * {@link AuthTokenOption} values. Default value of AuthTokenOption = DO_NOT_FORCE_REFRESH.
+ */
+ @NonNull
+ @Override
+ public Task 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.
+ *
+ * @return a new version of the prefs that includes the new FID. These prefs will have already
+ * been persisted.
+ */
+ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() {
+ FileLock fileLock = getCrossProcessLock();
+ try {
+ synchronized (lockGenerateFid) {
+ 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);
+ }
+ }
+
+ /** 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);
+ }
+ }
+
+ /** 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()) || !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 = fidGenerator.createRandomFid();
+ }
+ return fid;
+ }
+
+ /** 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. 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();
+ }
+ }
+
+ /**
+ * Deletes the firebase installation id of the {@link FirebaseApp} from FIS servers and local
+ * storage.
+ */
+ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException, IOException {
+ PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue();
+ if (entry.isRegistered()) {
+ // Call the FIS servers to delete this Firebase Installation Id.
+ try {
+ serviceClient.deleteFirebaseInstallation(
+ firebaseApp.getOptions().getApiKey(),
+ entry.getFirebaseInstallationId(),
+ firebaseApp.getOptions().getProjectId(),
+ entry.getRefreshToken());
+
+ } catch (FirebaseException exception) {
+ throw new FirebaseInstallationsException(
+ "Failed to delete a Firebase Installation.", Status.BAD_CONFIG);
+ }
+ }
+
+ 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
new file mode 100644
index 00000000000..07683203570
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java
@@ -0,0 +1,56 @@
+// 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 com.google.firebase.FirebaseException;
+
+/** The class for all Exceptions thrown by {@link FirebaseInstallations}. */
+public class FirebaseInstallationsException extends FirebaseException {
+ public enum Status {
+ /**
+ * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key or
+ * Project.
+ */
+ BAD_CONFIG,
+ }
+
+ @NonNull private final Status status;
+
+ public FirebaseInstallationsException(@NonNull Status status) {
+ this.status = status;
+ }
+
+ public FirebaseInstallationsException(@NonNull String message, @NonNull Status status) {
+ super(message);
+ this.status = status;
+ }
+
+ public FirebaseInstallationsException(
+ @NonNull String message, @NonNull Status status, @NonNull Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ }
+
+ /**
+ * Gets the status for the operation that failed.
+ *
+ * @return the status for the FirebaseInstallationsException
+ */
+ @NonNull
+ public Status getStatus() {
+ return status;
+ }
+}
diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java
new file mode 100644
index 00000000000..10731b9d11e
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java
@@ -0,0 +1,48 @@
+// 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.Keep;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.components.Component;
+import com.google.firebase.components.ComponentRegistrar;
+import com.google.firebase.components.Dependency;
+import com.google.firebase.heartbeatinfo.HeartBeatInfo;
+import com.google.firebase.platforminfo.LibraryVersionComponent;
+import com.google.firebase.platforminfo.UserAgentPublisher;
+import java.util.Arrays;
+import java.util.List;
+
+/** @hide */
+@Keep
+public class FirebaseInstallationsRegistrar implements ComponentRegistrar {
+
+ @Override
+ public List 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
new file mode 100644
index 00000000000..8cd08bdb299
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java
@@ -0,0 +1,31 @@
+// 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 com.google.firebase.installations.local.PersistedInstallationEntry;
+
+interface StateListener {
+ /**
+ * Returns {@code true} if the defined {@link PersistedInstallationEntry} state is reached, {@code
+ * false} otherwise.
+ */
+ boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry);
+
+ /**
+ * Returns {@code true} if an exception is thrown while registering a Firebase Installation,
+ * {@code false} otherwise.
+ */
+ boolean onException(PersistedInstallationEntry persistedInstallationEntry, Exception exception);
+}
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
new file mode 100644
index 00000000000..d2f748ce734
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java
@@ -0,0 +1,50 @@
+// 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 android.text.TextUtils;
+import com.google.firebase.installations.local.PersistedInstallationEntry;
+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;
+
+ 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 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(calendar.getTimeInMillis());
+ }
+}
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
new file mode 100644
index 00000000000..4e8366df7bf
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java
@@ -0,0 +1,135 @@
+// 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.local;
+
+import static android.content.ContentValues.TAG;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Base64;
+import android.util.Log;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.firebase.FirebaseApp;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Read existing iid only for default (first initialized) instance of this firebase application.*
+ */
+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";
+
+ @GuardedBy("iidPrefs")
+ private final SharedPreferences iidPrefs;
+
+ 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.
+ iidPrefs =
+ FirebaseApp.getInstance()
+ .getApplicationContext()
+ .getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ @Nullable
+ public String readIid() {
+ synchronized (iidPrefs) {
+ // Background: Some versions of the IID-SDK store the Instance-ID in local storage,
+ // others only store the App-Instance's Public-Key that can be used to calculate the
+ // Instance-ID.
+
+ // If such a version was used by this App-Instance, we can directly read the existing
+ // Instance-ID from storage and return it
+ String id = readInstanceIdFromLocalStorage();
+
+ if (id != null) {
+ return id;
+ }
+
+ // If this App-Instance did not store the Instance-ID in local storage, we may be able to find
+ // its Public-Key in order to calculate the App-Instance's Instance-ID.
+ return readPublicKeyFromLocalStorageAndCalculateInstanceId();
+ }
+ }
+
+ @Nullable
+ private String readInstanceIdFromLocalStorage() {
+ synchronized (iidPrefs) {
+ return iidPrefs.getString(STORE_KEY_ID, /* defaultValue= */ null);
+ }
+ }
+
+ @Nullable
+ private String readPublicKeyFromLocalStorageAndCalculateInstanceId() {
+ synchronized (iidPrefs) {
+ String base64PublicKey = iidPrefs.getString(STORE_KEY_PUB, /* defaultValue= */ null);
+ if (base64PublicKey == null) {
+ return null;
+ }
+
+ PublicKey publicKey = parseKey(base64PublicKey);
+ if (publicKey == null) {
+ return null;
+ }
+
+ return getIdFromPublicKey(publicKey);
+ }
+ }
+
+ @Nullable
+ private static String getIdFromPublicKey(@NonNull PublicKey publicKey) {
+ // The ID is the sha of the public key truncated to 60 bit, with first 4 bits switched to
+ // 0x9 and base64 encoded
+ // This allows the id to be used internally for legacy systems and differentiate from
+ // old android ids and gcm ids
+
+ byte[] derPub = publicKey.getEncoded();
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+
+ byte[] digest = md.digest(derPub);
+ int b0 = digest[0];
+ b0 = 0x70 + (0xF & b0);
+ digest[0] = (byte) (b0 & 0xFF);
+ return Base64.encodeToString(
+ digest, 0, 8, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "Unexpected error, device missing required algorithms");
+ }
+ return null;
+ }
+
+ /** Parse the public key from stored data. */
+ @Nullable
+ private PublicKey parseKey(String base64PublicKey) {
+ byte[] publicKeyBytes;
+ try {
+ publicKeyBytes = Base64.decode(base64PublicKey, Base64.URL_SAFE);
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return kf.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
+ } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException e) {
+ Log.w(TAG, "Invalid key stored " + e);
+ }
+ return null;
+ }
+}
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
new file mode 100644
index 00000000000..d3786c4b6e4
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java
@@ -0,0 +1,176 @@
+// 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.local;
+
+import androidx.annotation.NonNull;
+import com.google.firebase.FirebaseApp;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+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 written to
+ // local storage 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.
+ */
+ NOT_GENERATED,
+ /**
+ * {@link PersistedInstallationEntry} is not synced with FIS servers. Next state: REGISTERED -
+ * If FID registration is successful. REGISTER_ERROR - If FID registration or refresh auth token
+ * failed.
+ */
+ UNREGISTERED,
+ /**
+ * {@link PersistedInstallationEntry} is synced to FIS servers. Next state: REGISTER_ERROR - If
+ * FID registration or refresh auth token failed.
+ */
+ REGISTERED,
+ /**
+ * {@link PersistedInstallationEntry} is in error state when an exception is thrown while
+ * syncing with FIS server. Next state: UNREGISTERED - A new FID is created and persisted
+ * locally before registering with FIS servers.
+ */
+ REGISTER_ERROR,
+ }
+
+ 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";
+ private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs";
+ private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs";
+ private static final String PERSISTED_STATUS_KEY = "Status";
+ private static final String FIS_ERROR_KEY = "FisError";
+
+ public PersistedInstallation(@NonNull FirebaseApp firebaseApp) {
+ // Different FirebaseApp in the same Android application should have the same application
+ // context and same dir path
+ dataFile =
+ new File(
+ firebaseApp.getApplicationContext().getFilesDir(),
+ SETTINGS_FILE_NAME_PREFIX + "." + firebaseApp.getPersistenceKey() + ".json");
+ this.firebaseApp = firebaseApp;
+ }
+
+ @NonNull
+ public PersistedInstallationEntry readPersistedInstallationEntryValue() {
+ JSONObject json = readJSONFromFile();
+
+ 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() {
+ 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 new JSONObject(baos.toString());
+ } catch (IOException | JSONException e) {
+ return new JSONObject();
+ }
+ }
+
+ /**
+ * 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(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_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);
+ 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_PREFIX);
+ }
+ } catch (JSONException | IOException e) {
+ // 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.
+ }
+
+ // 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;
+ }
+
+ /** 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
new file mode 100644
index 00000000000..f58f49a6afb
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java
@@ -0,0 +1,164 @@
+// 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.local;
+
+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
+ * few Firebase Installation attributes and the persisted status of this entry.
+ */
+@AutoValue
+public abstract class PersistedInstallationEntry {
+
+ @Nullable
+ public abstract String getFirebaseInstallationId();
+
+ @NonNull
+ public abstract PersistedInstallation.RegistrationStatus getRegistrationStatus();
+
+ @Nullable
+ public abstract String getAuthToken();
+
+ @Nullable
+ public abstract String getRefreshToken();
+
+ public abstract long getExpiresInSecs();
+
+ public abstract long getTokenCreationEpochInSecs();
+
+ @Nullable
+ public abstract String getFisError();
+
+ @NonNull
+ public static PersistedInstallationEntry INSTANCE = PersistedInstallationEntry.builder().build();
+
+ public boolean isRegistered() {
+ return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTERED;
+ }
+
+ public boolean isErrored() {
+ return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTER_ERROR;
+ }
+
+ public boolean isUnregistered() {
+ return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.UNREGISTERED;
+ }
+
+ public boolean isNotGenerated() {
+ 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
+ public abstract Builder toBuilder();
+
+ /** Returns a default Builder object to create an PersistedInstallationEntry object */
+ @NonNull
+ public static PersistedInstallationEntry.Builder builder() {
+ return new AutoValue_PersistedInstallationEntry.Builder()
+ .setTokenCreationEpochInSecs(0)
+ .setRegistrationStatus(RegistrationStatus.ATTEMPT_MIGRATION)
+ .setExpiresInSecs(0);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ @NonNull
+ public abstract Builder setFirebaseInstallationId(@NonNull String value);
+
+ @NonNull
+ public abstract Builder setRegistrationStatus(
+ @NonNull PersistedInstallation.RegistrationStatus value);
+
+ @NonNull
+ public abstract Builder setAuthToken(@Nullable String value);
+
+ @NonNull
+ public abstract Builder setRefreshToken(@Nullable String value);
+
+ @NonNull
+ public abstract Builder setExpiresInSecs(long value);
+
+ @NonNull
+ public abstract Builder setTokenCreationEpochInSecs(long value);
+
+ @NonNull
+ public abstract Builder setFisError(@Nullable String value);
+
+ @NonNull
+ public abstract PersistedInstallationEntry build();
+ }
+}
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
new file mode 100644
index 00000000000..bb392f9472e
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java
@@ -0,0 +1,403 @@
+// 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.remote;
+
+import static android.content.ContentValues.TAG;
+import static com.google.android.gms.common.internal.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.JsonReader;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.gms.common.util.AndroidUtilsLight;
+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.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;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.HttpsURLConnection;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Http client that sends request to Firebase Installations backend API. */
+public class FirebaseInstallationServiceClient {
+ private static final String FIREBASE_INSTALLATIONS_API_DOMAIN =
+ "firebaseinstallations.googleapis.com";
+ private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations";
+ private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT =
+ "projects/%s/installations/%s/authTokens:generate";
+ private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s";
+ private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1";
+ private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_v2";
+
+ private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type";
+ private static final String ACCEPT_HEADER_KEY = "Accept";
+ private static final String JSON_CONTENT_TYPE = "application/json";
+ private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding";
+ private static final String GZIP_CONTENT_ENCODING = "gzip";
+
+ /** Heartbeat tag for firebase installations. */
+ private static final String FIREBASE_INSTALLATIONS_ID_HEARTBEAT_TAG = "fire-installations-id";
+
+ private static final String HEART_BEAT_HEADER = "x-firebase-client-log-type";
+ private static final String USER_AGENT_HEADER = "x-firebase-client";
+
+ 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 int NETWORK_TIMEOUT_MILLIS = 10000;
+
+ private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s");
+
+ private static final int MAX_RETRIES = 1;
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ @VisibleForTesting
+ static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp.";
+
+ private final Context context;
+ private final UserAgentPublisher userAgentPublisher;
+ private final HeartBeatInfo heartbeatInfo;
+
+ public FirebaseInstallationServiceClient(
+ @NonNull Context context,
+ @Nullable UserAgentPublisher publisher,
+ @Nullable HeartBeatInfo heartBeatInfo) {
+ this.context = context;
+ this.userAgentPublisher = publisher;
+ this.heartbeatInfo = heartBeatInfo;
+ }
+
+ /**
+ * Creates a FID on the FIS Servers by calling FirebaseInstallations API create method.
+ *
+ * @param apiKey API Key that has access to FIS APIs
+ * @param fid Firebase Installation Identifier
+ * @param projectID Project Id
+ * @param appId the identifier of a Firebase application
+ * @return {@link InstallationResponse} generated from the response body
+ *
+ *
+ */
+ @NonNull
+ public InstallationResponse createFirebaseInstallation(
+ @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId)
+ throws IOException {
+ String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID);
+ 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();
+
+ if (httpResponseCode == 200) {
+ return readCreateResponse(httpsURLConnection);
+ }
+
+ if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) {
+ retryCount++;
+ continue;
+ }
+
+ // 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)
+ throws JSONException {
+ JSONObject firebaseInstallationData = new JSONObject();
+ firebaseInstallationData.put("fid", fid);
+ firebaseInstallationData.put("appId", appId);
+ firebaseInstallationData.put("authVersion", FIREBASE_INSTALLATION_AUTH_VERSION);
+ firebaseInstallationData.put("sdkVersion", "t.1.1.0");
+ return firebaseInstallationData;
+ }
+
+ /**
+ * Deletes a FID on the FIS Servers by calling FirebaseInstallations API delete method.
+ *
+ * @param apiKey API Key that has access to FIS APIs
+ * @param fid Firebase Installation Identifier
+ * @param projectID Project Id
+ * @param refreshToken a token used to authenticate FIS requests
+ */
+ @NonNull
+ public void deleteFirebaseInstallation(
+ @NonNull String apiKey,
+ @NonNull String fid,
+ @NonNull String projectID,
+ @NonNull String refreshToken)
+ throws FirebaseException, IOException {
+ String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid);
+ 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();
+
+ if (httpResponseCode == 200 || httpResponseCode == 401 || httpResponseCode == 404) {
+ return;
+ }
+
+ if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) {
+ retryCount++;
+ continue;
+ }
+
+ throw new FirebaseInstallationsException(
+ "bad config while trying to delete FID", Status.BAD_CONFIG);
+ }
+
+ throw new IOException();
+ }
+
+ /**
+ * Generates a new auth token for a FID on the FIS Servers by calling FirebaseInstallations API
+ * generateAuthToken method.
+ *
+ * @param apiKey API Key that has access to FIS APIs
+ * @param fid Firebase Installation Identifier
+ * @param projectID Project Id
+ * @param refreshToken a token used to authenticate FIS requests
+ *
+ *
+ */
+ @NonNull
+ public TokenResult generateAuthToken(
+ @NonNull String apiKey,
+ @NonNull String fid,
+ @NonNull String projectID,
+ @NonNull String refreshToken)
+ throws IOException {
+ String resourceName =
+ String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid);
+ 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);
+ }
+
+ if (httpResponseCode == 401 || httpResponseCode == 404) {
+ return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build();
+ }
+
+ if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) {
+ retryCount++;
+ continue;
+ }
+
+ return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build();
+ }
+ throw new IOException();
+ }
+
+ private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException {
+ HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
+ httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS);
+ httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS);
+ httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE);
+ httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE);
+ httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING);
+ httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
+ if (heartbeatInfo != null && userAgentPublisher != null) {
+ HeartBeat heartbeat = heartbeatInfo.getHeartBeatCode(FIREBASE_INSTALLATIONS_ID_HEARTBEAT_TAG);
+ if (heartbeat != HeartBeat.NONE) {
+ httpsURLConnection.addRequestProperty(USER_AGENT_HEADER, userAgentPublisher.getUserAgent());
+ httpsURLConnection.addRequestProperty(
+ HEART_BEAT_HEADER, Integer.toString(heartbeat.getCode()));
+ }
+ }
+ httpsURLConnection.addRequestProperty(
+ X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage());
+ return httpsURLConnection;
+ }
+
+ // Read the response from the createFirebaseInstallation API.
+ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException {
+ JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8));
+ TokenResult.Builder tokenResult = TokenResult.builder();
+ InstallationResponse.Builder builder = InstallationResponse.builder();
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals("name")) {
+ builder.setUri(reader.nextString());
+ } else if (name.equals("fid")) {
+ builder.setFid(reader.nextString());
+ } else if (name.equals("refreshToken")) {
+ builder.setRefreshToken(reader.nextString());
+ } else if (name.equals("authToken")) {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String key = reader.nextName();
+ if (key.equals("token")) {
+ tokenResult.setToken(reader.nextString());
+ } else if (key.equals("expiresIn")) {
+ tokenResult.setTokenExpirationTimestamp(
+ parseTokenExpirationTimestamp(reader.nextString()));
+ } else {
+ reader.skipValue();
+ }
+ }
+ builder.setAuthToken(tokenResult.build());
+ reader.endObject();
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+
+ return builder.setResponseCode(ResponseCode.OK).build();
+ }
+
+ // Read the response from the generateAuthToken FirebaseInstallation API.
+ private TokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) throws IOException {
+ JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8));
+ TokenResult.Builder builder = TokenResult.builder();
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (name.equals("token")) {
+ builder.setToken(reader.nextString());
+ } else if (name.equals("expiresIn")) {
+ builder.setTokenExpirationTimestamp(parseTokenExpirationTimestamp(reader.nextString()));
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+
+ return builder.setResponseCode(TokenResult.ResponseCode.OK).build();
+ }
+
+ // Read the error message from the response.
+ private String readErrorResponse(HttpsURLConnection conn) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), UTF_8));
+ StringBuilder response = new StringBuilder();
+ for (String input = reader.readLine(); input != null; input = reader.readLine()) {
+ response.append(input).append('\n');
+ }
+ return String.format(
+ "The server responded with an error. HTTP response: [%d %s %s]",
+ conn.getResponseCode(), conn.getResponseMessage(), response);
+ }
+
+ /** Gets the Android package's SHA-1 fingerprint. */
+ private String getFingerprintHashForPackage() {
+ byte[] hash;
+
+ try {
+ hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName());
+
+ if (hash == null) {
+ Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName());
+ return null;
+ } else {
+ return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "No such package: " + context.getPackageName(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns parsed token expiration timestamp in seconds.
+ *
+ * @param expiresIn is expiration timestamp in String format: 604800s
+ */
+ @VisibleForTesting
+ static long parseTokenExpirationTimestamp(String expiresIn) {
+ checkArgument(
+ EXPIRATION_TIMESTAMP_PATTERN.matcher(expiresIn).matches(),
+ PARSING_EXPIRATION_TIME_ERROR_MESSAGE);
+ return (expiresIn == null || expiresIn.length() == 0)
+ ? 0L
+ : Long.parseLong(expiresIn.substring(0, expiresIn.length() - 1));
+ }
+}
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
new file mode 100644
index 00000000000..213b4d19416
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java
@@ -0,0 +1,76 @@
+// 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.remote;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class InstallationResponse {
+
+ public enum ResponseCode {
+ // Returned on success
+ OK,
+ // 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
+ public abstract String getUri();
+
+ @Nullable
+ public abstract String getFid();
+
+ @Nullable
+ public abstract String getRefreshToken();
+
+ @Nullable
+ public abstract TokenResult getAuthToken();
+
+ @Nullable
+ public abstract ResponseCode getResponseCode();
+
+ @NonNull
+ public abstract Builder toBuilder();
+
+ /** Returns a default Builder object to create an InstallationResponse object */
+ @NonNull
+ public static InstallationResponse.Builder builder() {
+ return new AutoValue_InstallationResponse.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ @NonNull
+ public abstract Builder setUri(@NonNull String value);
+
+ @NonNull
+ public abstract Builder setFid(@NonNull String value);
+
+ @NonNull
+ public abstract Builder setRefreshToken(@NonNull String value);
+
+ @NonNull
+ public abstract Builder setAuthToken(@NonNull TokenResult value);
+
+ @NonNull
+ public abstract Builder setResponseCode(@NonNull ResponseCode value);
+
+ @NonNull
+ public abstract InstallationResponse build();
+ }
+}
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
new file mode 100644
index 00000000000..1fd7a5d99fd
--- /dev/null
+++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java
@@ -0,0 +1,69 @@
+// 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.remote;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+
+/** This class represents a set of values describing a FIS Auth Token Result. */
+@AutoValue
+public abstract class TokenResult {
+
+ public enum ResponseCode {
+ // Returned on success
+ 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.
+ 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.
+ AUTH_ERROR,
+ }
+
+ /** A new FIS Auth-Token, created for this Firebase Installation. */
+ @Nullable
+ public abstract String getToken();
+ /** The timestamp, before the auth-token expires for this Firebase Installation. */
+ @NonNull
+ public abstract long getTokenExpirationTimestamp();
+
+ @Nullable
+ public abstract ResponseCode getResponseCode();
+
+ @NonNull
+ public abstract Builder toBuilder();
+
+ /** Returns a default Builder object to create an InstallationResponse object */
+ @NonNull
+ public static TokenResult.Builder builder() {
+ return new AutoValue_TokenResult.Builder().setTokenExpirationTimestamp(0);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ @NonNull
+ public abstract Builder setToken(@NonNull String value);
+
+ @NonNull
+ public abstract Builder setTokenExpirationTimestamp(long value);
+
+ @NonNull
+ public abstract Builder setResponseCode(@NonNull ResponseCode value);
+
+ @NonNull
+ public abstract TokenResult build();
+ }
+}
diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java
new file mode 100644
index 00000000000..f553b7d701a
--- /dev/null
+++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java
@@ -0,0 +1,55 @@
+// 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 static org.junit.Assert.assertNotNull;
+
+import androidx.test.core.app.ApplicationProvider;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link FirebaseInstallationsRegistrar}. */
+@RunWith(RobolectricTestRunner.class)
+public class FirebaseInstallationsRegistrarTest {
+ @Before
+ public void setUp() {
+ FirebaseApp.clearInstancesForTest();
+ }
+
+ @Test
+ public void getFirebaseInstallationsInstance() {
+ FirebaseApp defaultApp =
+ FirebaseApp.initializeApp(
+ ApplicationProvider.getApplicationContext(),
+ new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build());
+
+ FirebaseApp anotherApp =
+ FirebaseApp.initializeApp(
+ ApplicationProvider.getApplicationContext(),
+ new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(),
+ "firebase_app_1");
+
+ FirebaseInstallations defaultFirebaseInstallation = FirebaseInstallations.getInstance();
+ assertNotNull(defaultFirebaseInstallation);
+
+ FirebaseInstallations anotherFirebaseInstallation =
+ FirebaseInstallations.getInstance(anotherApp);
+ assertNotNull(anotherFirebaseInstallation);
+ }
+}
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
new file mode 100644
index 00000000000..fe64dcf06b8
--- /dev/null
+++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java
@@ -0,0 +1,18 @@
+// 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;
+
+/** Tests for {@link FirebaseInstallations}. */
+public class FirebaseInstallationsTest {}
diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java
new file mode 100644
index 00000000000..eb30312c0e4
--- /dev/null
+++ b/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java
@@ -0,0 +1,55 @@
+// 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.remote;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link FirebaseInstallationServiceClient}. */
+@RunWith(RobolectricTestRunner.class)
+public class FirebaseInstallationServiceClientTest {
+
+ private final String TEST_EXPIRATION_TIMESTAMP = "604800s";
+ private final long TEST_EXPIRATION_IN_SECS = 604800;
+ private final String INCORRECT_EXPIRATION_TIMESTAMP = "2345";
+
+ @Test
+ public void parseTokenExpirationTimestamp_successful() {
+ long actual =
+ FirebaseInstallationServiceClient.parseTokenExpirationTimestamp(TEST_EXPIRATION_TIMESTAMP);
+
+ assertWithMessage("Exception status doesn't match")
+ .that(actual)
+ .isEqualTo(TEST_EXPIRATION_IN_SECS);
+ }
+
+ @Test
+ public void parseTokenExpirationTimestamp_failed() {
+ try {
+ FirebaseInstallationServiceClient.parseTokenExpirationTimestamp(
+ INCORRECT_EXPIRATION_TIMESTAMP);
+ fail("Parsing token expiration timestamp failed.");
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected)
+ .hasMessageThat()
+ .isEqualTo(FirebaseInstallationServiceClient.PARSING_EXPIRATION_TIME_ERROR_MESSAGE);
+ }
+ }
+}
diff --git a/subprojects.cfg b/subprojects.cfg
index dd36b83abc5..33d16449cdc 100644
--- a/subprojects.cfg
+++ b/subprojects.cfg
@@ -21,6 +21,8 @@ firebase-inappmessaging
firebase-inappmessaging:ktx
firebase-inappmessaging-display
firebase-inappmessaging-display:ktx
+firebase-installations-interop
+firebase-installations
firebase-storage
firebase-storage:ktx
firebase-storage:test-app