diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index dc4606715c5..1f76b8b593a 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -83,13 +83,18 @@ android { dependencies { implementation project(':firebase-common') + implementation project(':protolite-well-known-types') implementation('com.google.firebase:firebase-iid:17.0.3') { exclude group: "com.google.firebase", module: "firebase-common" } + implementation 'io.grpc:grpc-stub:1.21.0' + implementation 'io.grpc:grpc-protobuf-lite:1.21.0' + implementation 'io.grpc:grpc-okhttp:1.21.0' implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.android.gms:play-services-tasks:16.0.1' + implementation 'com.squareup.okhttp:okhttp:2.7.5' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" @@ -104,4 +109,6 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'junit:junit:4.12' + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 739df092564..8519e641cb7 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -14,15 +14,34 @@ package com.google.firebase.segmentation; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import java.util.concurrent.ExecutionException; +import org.junit.After; import org.junit.Before; +import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Instrumented test, which will execute on an Android device. @@ -30,21 +49,183 @@ * @see Testing documentation */ @RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class FirebaseSegmentationInstrumentedTest { + private static final String CUSTOM_INSTALLATION_ID = "123"; + private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; + private FirebaseApp firebaseApp; + @Mock private FirebaseInstanceId firebaseInstanceId; + @Mock private SegmentationServiceClient backendClientReturnsOk; + @Mock private SegmentationServiceClient backendClientReturnsError; + private CustomInstallationIdCache actualCache; + @Mock private CustomInstallationIdCache cacheReturnsError; @Before public void setUp() { + MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + actualCache = new CustomInstallationIdCache(firebaseApp); + + when(backendClientReturnsOk.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(firebaseInstanceId.getInstanceId()) + .thenReturn( + Tasks.forResult( + new InstanceIdResult() { + @NonNull + @Override + public String getId() { + return FIREBASE_INSTANCE_ID; + } + + @NonNull + @Override + public String getToken() { + return "iid_token"; + } + })); + when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(Tasks.forResult(false)); + when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); + } + + @After + public void cleanUp() throws Exception { + Tasks.await(actualCache.clear()); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendError() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } @Test - public void useAppContext() { - assertNull(FirebaseSegmentation.getInstance().setCustomInstallationId("123123").getResult()); + public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(null))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertNull(entryValue); + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(null)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId().isEmpty()).isTrue(); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_CLEAR); + } + + @Test + public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } } } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 27ab706b3a8..019a2b8ba08 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -34,7 +34,8 @@ public class CustomInstallationIdCacheTest { private FirebaseApp firebaseApp0; private FirebaseApp firebaseApp1; - private CustomInstallationIdCache cache; + private CustomInstallationIdCache cache0; + private CustomInstallationIdCache cache1; @Before public void setUp() { @@ -48,42 +49,44 @@ public void setUp() { ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); - cache = CustomInstallationIdCache.getInstance(); + cache0 = new CustomInstallationIdCache(firebaseApp0); + cache1 = new CustomInstallationIdCache(firebaseApp1); } @After public void cleanUp() throws Exception { - Tasks.await(cache.clearAll()); + Tasks.await(cache0.clear()); + Tasks.await(cache1.clear()); } @Test public void testReadCacheEntry_Null() { - assertNull(cache.readCacheEntryValue(firebaseApp0)); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + assertNull(cache0.readCacheEntryValue()); + assertNull(cache1.readCacheEntryValue()); } @Test public void testUpdateAndReadCacheEntry() throws Exception { assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)))); - CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); + "123456", + "cAAAAAAAAAA", + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)))); + CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); + assertNull(cache1.readCacheEntryValue()); assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); - entryValue = cache.readCacheEntryValue(firebaseApp0); + entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index eca517db1b3..34de68597c1 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -15,21 +15,45 @@ package com.google.firebase.segmentation; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.SetCustomInstallationIdException.Status; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { private final FirebaseApp firebaseApp; private final FirebaseInstanceId firebaseInstanceId; + private final CustomInstallationIdCache localCache; + private final SegmentationServiceClient backendServiceClient; FirebaseSegmentation(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); + localCache = new CustomInstallationIdCache(firebaseApp); + backendServiceClient = new SegmentationServiceClient(); + } + + @RestrictTo(RestrictTo.Scope.TESTS) + FirebaseSegmentation( + FirebaseApp firebaseApp, + FirebaseInstanceId firebaseInstanceId, + CustomInstallationIdCache localCache, + SegmentationServiceClient backendServiceClient) { + this.firebaseApp = firebaseApp; + this.firebaseInstanceId = firebaseInstanceId; + this.localCache = localCache; + this.backendServiceClient = backendServiceClient; } /** @@ -55,7 +79,162 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(String customInstallationId) { - return Tasks.forResult(null); + @NonNull + public synchronized Task 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()); } }