setCustomInstallationId(@Nullable String customInstallationId) {
+ if (customInstallationId == null) {
+ return clearCustomInstallationId();
+ }
+ return updateCustomInstallationId(customInstallationId);
+ }
+
+ /**
+ * Update custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and
+ * client side cache.
+ *
+ *
+ * The workflow is:
+ * check diff against cache or cache status is not SYNCED
+ * |
+ * get Firebase instance id and token
+ * | |
+ * | update cache with cache status PENDING_UPDATE
+ * | |
+ * send http request to backend
+ * |
+ * on success: set cache entry status to SYNCED
+ * |
+ * return
+ *
+ */
+ private Task updateCustomInstallationId(String customInstallationId) {
+ CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue();
+ if (cacheEntryValue != null
+ && cacheEntryValue.getCustomInstallationId().equals(customInstallationId)
+ && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) {
+ // If the given custom installation id matches up the cached
+ // value, there's no need to update.
+ return Tasks.forResult(null);
+ }
+
+ Task instanceIdResultTask = firebaseInstanceId.getInstanceId();
+ Task firstUpdateCacheResultTask =
+ instanceIdResultTask.onSuccessTask(
+ instanceIdResult ->
+ localCache.insertOrUpdateCacheEntry(
+ CustomInstallationIdCacheEntryValue.create(
+ customInstallationId,
+ instanceIdResult.getId(),
+ CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)));
+
+ // Start requesting backend when first cache update is done.
+ Task backendRequestResultTask =
+ firstUpdateCacheResultTask.onSuccessTask(
+ firstUpdateCacheResult -> {
+ if (firstUpdateCacheResult) {
+ String iid = instanceIdResultTask.getResult().getId();
+ String iidToken = instanceIdResultTask.getResult().getToken();
+ return backendServiceClient.updateCustomInstallationId(
+ Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()),
+ customInstallationId,
+ iid,
+ iidToken);
+ } else {
+ throw new SetCustomInstallationIdException(
+ "Failed to update client side cache", Status.CLIENT_ERROR);
+ }
+ });
+
+ Task finalUpdateCacheResultTask =
+ backendRequestResultTask.onSuccessTask(
+ backendRequestResult -> {
+ switch (backendRequestResult) {
+ case OK:
+ return localCache.insertOrUpdateCacheEntry(
+ CustomInstallationIdCacheEntryValue.create(
+ customInstallationId,
+ instanceIdResultTask.getResult().getId(),
+ CustomInstallationIdCache.CacheStatus.SYNCED));
+ case ALREADY_EXISTS:
+ throw new SetCustomInstallationIdException(
+ Status.DUPLICATED_CUSTOM_INSTALLATION_ID);
+ default:
+ throw new SetCustomInstallationIdException(Status.BACKEND_ERROR);
+ }
+ });
+
+ return finalUpdateCacheResultTask.onSuccessTask(
+ finalUpdateCacheResult -> {
+ if (finalUpdateCacheResult) {
+ return Tasks.forResult(null);
+ } else {
+ throw new SetCustomInstallationIdException(
+ "Failed to update client side cache", Status.CLIENT_ERROR);
+ }
+ });
+ }
+
+ /**
+ * Clear custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and
+ * client side cache.
+ *
+ *
+ * The workflow is:
+ * get Firebase instance id and token
+ * | |
+ * | update cache with cache status PENDING_CLEAR
+ * | |
+ * send http request to backend
+ * |
+ * on success: delete cache entry
+ * |
+ * return
+ *
+ */
+ private Task clearCustomInstallationId() {
+ Task instanceIdResultTask = firebaseInstanceId.getInstanceId();
+ Task firstUpdateCacheResultTask =
+ instanceIdResultTask.onSuccessTask(
+ instanceIdResult ->
+ localCache.insertOrUpdateCacheEntry(
+ CustomInstallationIdCacheEntryValue.create(
+ "",
+ instanceIdResult.getId(),
+ CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)));
+
+ Task backendRequestResultTask =
+ firstUpdateCacheResultTask.onSuccessTask(
+ firstUpdateCacheResult -> {
+ if (firstUpdateCacheResult) {
+ String iid = instanceIdResultTask.getResult().getId();
+ String iidToken = instanceIdResultTask.getResult().getToken();
+ return backendServiceClient.clearCustomInstallationId(
+ Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()),
+ iid,
+ iidToken);
+ } else {
+ throw new SetCustomInstallationIdException(
+ "Failed to update client side cache", Status.CLIENT_ERROR);
+ }
+ });
+
+ Task finalUpdateCacheResultTask =
+ backendRequestResultTask.onSuccessTask(
+ backendRequestResult -> {
+ if (backendRequestResult == Code.OK) {
+ return localCache.clear();
+ } else {
+ throw new SetCustomInstallationIdException(Status.BACKEND_ERROR);
+ }
+ });
+
+ return finalUpdateCacheResultTask.onSuccessTask(
+ finalUpdateCacheResult -> {
+ if (finalUpdateCacheResult) {
+ return Tasks.forResult(null);
+ } else {
+ throw new SetCustomInstallationIdException(
+ "Failed to update client side cache", Status.CLIENT_ERROR);
+ }
+ });
}
}
diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java
new file mode 100644
index 00000000000..3c957ce3294
--- /dev/null
+++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java
@@ -0,0 +1,68 @@
+// 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.segmentation;
+
+import androidx.annotation.NonNull;
+import com.google.firebase.FirebaseException;
+
+/** The class for all Exceptions thrown by {@link FirebaseSegmentation}. */
+public class SetCustomInstallationIdException extends FirebaseException {
+
+ public enum Status {
+ UNKOWN(0),
+
+ /** Error in Firebase SDK. */
+ CLIENT_ERROR(1),
+
+ /** Error when calling Firebase segmentation backend. */
+ BACKEND_ERROR(2),
+
+ /** The given custom installation is already tied to another Firebase installation. */
+ DUPLICATED_CUSTOM_INSTALLATION_ID(3);
+
+ private final int value;
+
+ Status(int value) {
+ this.value = value;
+ }
+ }
+
+ @NonNull private final Status status;
+
+ SetCustomInstallationIdException(@NonNull Status status) {
+ this.status = status;
+ }
+
+ SetCustomInstallationIdException(@NonNull String message, @NonNull Status status) {
+ super(message);
+ this.status = status;
+ }
+
+ SetCustomInstallationIdException(
+ @NonNull String message, @NonNull Status status, Throwable cause) {
+ super(message, cause);
+ this.status = status;
+ }
+
+ /**
+ * Gets the status code for the operation that failed.
+ *
+ * @return the code for the SetCustomInstallationIdException
+ */
+ @NonNull
+ public Status getStatus() {
+ return status;
+ }
+}
diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java
new file mode 100644
index 00000000000..ca231a89cb5
--- /dev/null
+++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java
@@ -0,0 +1,33 @@
+// 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.segmentation;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Util methods used for {@link FirebaseSegmentation} */
+class Utils {
+
+ private static final Pattern APP_ID_PATTERN =
+ Pattern.compile("^[^:]+:([0-9]+):(android|ios|web):([0-9a-f]+)");
+
+ static long getProjectNumberFromAppId(String appId) {
+ Matcher matcher = APP_ID_PATTERN.matcher(appId);
+ if (matcher.matches()) {
+ return Long.valueOf(matcher.group(1));
+ }
+ throw new IllegalArgumentException("Invalid app id " + appId);
+ }
+}
diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java
index 2b93231eb39..a1346062440 100644
--- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java
+++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java
@@ -17,27 +17,30 @@
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import com.google.android.gms.common.util.Strings;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.firebase.FirebaseApp;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
-class CustomInstallationIdCache {
+/**
+ * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation
+ * backend API.
+ */
+public class CustomInstallationIdCache {
// Status of each cache entry
// NOTE: never change the ordinal of the enum values because the enum values are stored in cache
// as their ordinal numbers.
- enum CacheStatus {
+ public enum CacheStatus {
// Cache entry is synced to Firebase backend
SYNCED,
- // Cache entry is waiting for Firebase backend response or pending internal retry for retryable
- // errors.
- PENDING,
- // Cache entry is not accepted by Firebase backend.
- ERROR,
+ // Cache entry is waiting for Firebase backend response or internal network retry (for update
+ // operation).
+ PENDING_UPDATE,
+ // Cache entry is waiting for Firebase backend response or internal network retry (for clear
+ // operation).
+ PENDING_CLEAR
}
private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache";
@@ -46,85 +49,59 @@ enum CacheStatus {
private static final String INSTANCE_ID_KEY = "Iid";
private static final String CACHE_STATUS_KEY = "Status";
- private static CustomInstallationIdCache singleton = null;
private final Executor ioExecuter;
private final SharedPreferences prefs;
+ private final String persistenceKey;
- static synchronized CustomInstallationIdCache getInstance() {
- if (singleton == null) {
- singleton = new CustomInstallationIdCache();
- }
- return singleton;
- }
-
- private CustomInstallationIdCache() {
- // Since different FirebaseApp in the same Android application should have the same application
- // context and same dir path, so that use the context of the default FirebaseApp to create the
- // shared preferences.
+ public CustomInstallationIdCache(FirebaseApp firebaseApp) {
+ // Different FirebaseApp in the same Android application should have the same application
+ // context and same dir path
prefs =
- FirebaseApp.getInstance()
+ firebaseApp
.getApplicationContext()
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
-
+ persistenceKey = firebaseApp.getPersistenceKey();
ioExecuter = Executors.newFixedThreadPool(2);
}
@Nullable
- synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) {
- String cid =
- prefs.getString(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), null);
- String iid = prefs.getString(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), null);
- int status = prefs.getInt(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), -1);
+ public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() {
+ String cid = prefs.getString(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), null);
+ String iid = prefs.getString(getSharedPreferencesKey(INSTANCE_ID_KEY), null);
+ int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1);
- if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) {
+ if (cid == null || iid == null || status == -1) {
return null;
}
return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]);
}
- synchronized Task insertOrUpdateCacheEntry(
- FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) {
+ public synchronized Task insertOrUpdateCacheEntry(
+ CustomInstallationIdCacheEntryValue entryValue) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(
- getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY),
- entryValue.getCustomInstallationId());
- editor.putString(
- getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId());
- editor.putInt(
- getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY),
- entryValue.getCacheStatus().ordinal());
- return commitSharedPreferencesEditAsync(editor);
- }
-
- synchronized Task clear(FirebaseApp firebaseApp) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.remove(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY));
- editor.remove(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY));
- editor.remove(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY));
+ getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId());
+ editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId());
+ editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal());
return commitSharedPreferencesEditAsync(editor);
}
- @RestrictTo(RestrictTo.Scope.TESTS)
- synchronized Task clearAll() {
+ public synchronized Task clear() {
SharedPreferences.Editor editor = prefs.edit();
- editor.clear();
+ editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY));
+ editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY));
+ editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY));
return commitSharedPreferencesEditAsync(editor);
}
- private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) {
- return String.format("%s|%s", firebaseApp.getPersistenceKey(), key);
+ private String getSharedPreferencesKey(String key) {
+ return String.format("%s|%s", persistenceKey, key);
}
private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) {
- TaskCompletionSource result = new TaskCompletionSource();
- ioExecuter.execute(
- new Runnable() {
- @Override
- public void run() {
- result.setResult(editor.commit());
- }
- });
+ TaskCompletionSource result = new TaskCompletionSource<>();
+ ioExecuter.execute(() -> result.setResult(editor.commit()));
return result.getTask();
}
}
diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java
index 2d3b5f3c3a6..05528cd40f0 100644
--- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java
+++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java
@@ -22,14 +22,14 @@
* Firebase instance id, a custom installation id and the cache status of this entry.
*/
@AutoValue
-abstract class CustomInstallationIdCacheEntryValue {
- abstract String getCustomInstallationId();
+public abstract class CustomInstallationIdCacheEntryValue {
+ public abstract String getCustomInstallationId();
- abstract String getFirebaseInstanceId();
+ public abstract String getFirebaseInstanceId();
- abstract CacheStatus getCacheStatus();
+ public abstract CacheStatus getCacheStatus();
- static CustomInstallationIdCacheEntryValue create(
+ public static CustomInstallationIdCacheEntryValue create(
String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) {
return new AutoValue_CustomInstallationIdCacheEntryValue(
customInstallationId, firebaseInstanceId, cacheStatus);
diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java
new file mode 100644
index 00000000000..59398b369d5
--- /dev/null
+++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.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.segmentation.remote;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+import com.squareup.okhttp.OkHttpClient;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** Http client that sends request to Firebase Segmentation backend API. To be implemented */
+public class SegmentationServiceClient {
+
+ private final OkHttpClient httpClient;
+ private final Executor httpRequestExecutor;
+
+ public enum Code {
+ OK,
+
+ SERVER_INTERNAL_ERROR,
+
+ ALREADY_EXISTS,
+
+ PERMISSION_DENIED
+ }
+
+ public SegmentationServiceClient() {
+ httpClient = new OkHttpClient();
+ httpRequestExecutor = Executors.newFixedThreadPool(4);
+ }
+
+ public Task updateCustomInstallationId(
+ long projectNumber,
+ String customInstallationId,
+ String firebaseInstanceId,
+ String firebaseInstanceIdToken) {
+ return Tasks.forResult(Code.OK);
+ }
+
+ public Task clearCustomInstallationId(
+ long projectNumber, String firebaseInstanceId, String firebaseInstanceIdToken) {
+ return Tasks.forResult(Code.OK);
+ }
+}
diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java
index 1f3c441808f..56b0d120eb0 100644
--- a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java
+++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java
@@ -15,7 +15,6 @@
package com.google.firebase.segmentation;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import androidx.test.core.app.ApplicationProvider;
import com.google.firebase.FirebaseApp;
@@ -48,10 +47,8 @@ public void getFirebaseInstallationsInstance() {
FirebaseSegmentation defaultSegmentation = FirebaseSegmentation.getInstance();
assertNotNull(defaultSegmentation);
- assertNull(defaultSegmentation.setCustomInstallationId("12345").getResult());
FirebaseSegmentation anotherSegmentation = FirebaseSegmentation.getInstance(anotherApp);
assertNotNull(anotherSegmentation);
- assertNull(anotherSegmentation.setCustomInstallationId("ghdjaas").getResult());
}
}