From 8881325d71e3863b3ec89971b6254c3316017010 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 13 Jun 2019 13:54:46 -0700 Subject: [PATCH 01/16] Implement Firebase segmentation SDK device local cache --- .../firebase-segmentation.gradle | 9 +- .../CustomInstallationIdMappingCache.java | 98 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 88ea12a1ed0..11a5046d6d3 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -57,7 +57,7 @@ android { compileSdkVersion project.targetSdkVersion defaultConfig { - minSdkVersion project.minSdkVersion + minSdkVersion 21 targetSdkVersion project.targetSdkVersion multiDexEnabled true versionName version @@ -95,8 +95,9 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" - androidTestImplementation 'androidx.annotation:annotation:1.1.0' - androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation "androidx.annotation:annotation:1.1.0" androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.12' } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java new file mode 100644 index 00000000000..9826ea9e23f --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java @@ -0,0 +1,98 @@ +// 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 android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.Preconditions; +import com.google.firebase.FirebaseApp; + +class CustomInstallationIdMappingCache { + + // Status of each cache entry + 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 + } + + private static final String LOCAL_DB_NAME = "CustomInstallationIdCache"; + private static final String TABLE_NAME = "InstallationIdMapping"; + + private static final String GMP_APP_ID_COLUMN_NAME = "GmpAppId"; + private static final String FIREBASE_APP_NAME_COLUMN_NAME = "AppName"; + private static final String INSTANCE_ID_COLUMN_NAME = "Iid"; + private static final String CUSTOM_INSTALLATION_ID_COLUMN_NAME = "Cid"; + private static final String CACHE_STATUS_COLUMN = "Status"; + + private static final String QUERY_WHERE_CLAUSE = + String.format( + "%s = ? " + "AND " + "%s = ?", GMP_APP_ID_COLUMN_NAME, FIREBASE_APP_NAME_COLUMN_NAME); + + private final SQLiteDatabase localDb; + + CustomInstallationIdMappingCache() { + // 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/open + // the database. + localDb = + SQLiteDatabase.openOrCreateDatabase( + FirebaseApp.getInstance() + .getApplicationContext() + .getNoBackupFilesDir() + .getAbsolutePath() + + "/" + + LOCAL_DB_NAME, + null); + + localDb.execSQL( + String.format( + "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT PRIMARY KEY, " + + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL);", + TABLE_NAME, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME, + INSTANCE_ID_COLUMN_NAME, + CUSTOM_INSTALLATION_ID_COLUMN_NAME, + CACHE_STATUS_COLUMN)); + } + + @Nullable + String readIid(FirebaseApp firebaseApp) { + String gmpAppId = firebaseApp.getOptions().getApplicationId(); + String appName = firebaseApp.getName(); + Cursor cursor = + localDb.query( + TABLE_NAME, + new String[] {INSTANCE_ID_COLUMN_NAME}, + QUERY_WHERE_CLAUSE, + new String[] {gmpAppId, appName}, + null, + null, + null); + String iid = null; + while (cursor.moveToNext()) { + Preconditions.checkArgument( + iid == null, "Multiple iid found for " + "firebase app %s", appName); + iid = cursor.getString(cursor.getColumnIndex(INSTANCE_ID_COLUMN_NAME)); + } + return iid; + } +} From 864748f4ac58f4662187be16d75084a16229e80f Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 14 Jun 2019 14:25:33 -0700 Subject: [PATCH 02/16] [Firebase Segmentation] Add custom installation id cache layer and tests for it. --- .../firebase-segmentation.gradle | 4 + .../CustomInstallationIdCacheTest.java | 77 +++++++++++++++++++ ...he.java => CustomInstallationIdCache.java} | 62 +++++++++++---- .../CustomInstallationIdCacheEntryValue.java | 37 +++++++++ 4 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{CustomInstallationIdMappingCache.java => CustomInstallationIdCache.java} (58%) create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 11a5046d6d3..cc24fe30ced 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -91,11 +91,15 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.android.gms:play-services-tasks:16.0.1' + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" + testImplementation 'androidx.test:core:1.2.0' testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" androidTestImplementation "androidx.annotation:annotation:1.1.0" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java new file mode 100644 index 00000000000..c6b7ce0cecb --- /dev/null +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -0,0 +1,77 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumented tests for {@link CustomInstallationIdCache} */ +@RunWith(AndroidJUnit4.class) +public class CustomInstallationIdCacheTest { + + private FirebaseApp firebaseApp0; + private FirebaseApp firebaseApp1; + private CustomInstallationIdCache cache; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp0 = + FirebaseApp.initializeApp( + InstrumentationRegistry.getContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + firebaseApp1 = + FirebaseApp.initializeApp( + InstrumentationRegistry.getContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + cache = new CustomInstallationIdCache(); + } + + @After + public void cleanUp() { + cache.clear(); + } + + @Test + public void testReadCacheEntry_Null() { + assertNull(cache.readCacheEntryValue(firebaseApp0)); + assertNull(cache.readCacheEntryValue(firebaseApp1)); + } + + @Test + public void testUpdateAndReadCacheEntry() { + cache.insertOrUpdateCacheEntry( + firebaseApp0, + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)); + CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); + assertNotNull(entryValue); + assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); + assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); + assertNull(cache.readCacheEntryValue(firebaseApp1)); + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java similarity index 58% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index 9826ea9e23f..afb1c86b5b0 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdMappingCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -17,12 +17,15 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; import com.google.firebase.FirebaseApp; -class CustomInstallationIdMappingCache { +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 { // Cache entry is synced to Firebase backend SYNCED, @@ -38,8 +41,8 @@ enum CacheStatus { private static final String GMP_APP_ID_COLUMN_NAME = "GmpAppId"; private static final String FIREBASE_APP_NAME_COLUMN_NAME = "AppName"; - private static final String INSTANCE_ID_COLUMN_NAME = "Iid"; private static final String CUSTOM_INSTALLATION_ID_COLUMN_NAME = "Cid"; + private static final String INSTANCE_ID_COLUMN_NAME = "Iid"; private static final String CACHE_STATUS_COLUMN = "Status"; private static final String QUERY_WHERE_CLAUSE = @@ -48,7 +51,7 @@ enum CacheStatus { private final SQLiteDatabase localDb; - CustomInstallationIdMappingCache() { + 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/open // the database. @@ -64,35 +67,68 @@ enum CacheStatus { localDb.execSQL( String.format( - "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT PRIMARY KEY, " - + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL);", + "CREATE TABLE IF NOT EXISTS %s(%s TEXT NOT NULL, %s TEXT NOT NULL, " + + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL, PRIMARY KEY (%s, %s));", TABLE_NAME, GMP_APP_ID_COLUMN_NAME, FIREBASE_APP_NAME_COLUMN_NAME, - INSTANCE_ID_COLUMN_NAME, CUSTOM_INSTALLATION_ID_COLUMN_NAME, - CACHE_STATUS_COLUMN)); + INSTANCE_ID_COLUMN_NAME, + CACHE_STATUS_COLUMN, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME)); } @Nullable - String readIid(FirebaseApp firebaseApp) { + CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { String gmpAppId = firebaseApp.getOptions().getApplicationId(); String appName = firebaseApp.getName(); Cursor cursor = localDb.query( TABLE_NAME, - new String[] {INSTANCE_ID_COLUMN_NAME}, + new String[] { + CUSTOM_INSTALLATION_ID_COLUMN_NAME, INSTANCE_ID_COLUMN_NAME, CACHE_STATUS_COLUMN + }, QUERY_WHERE_CLAUSE, new String[] {gmpAppId, appName}, null, null, null); - String iid = null; + CustomInstallationIdCacheEntryValue value = null; while (cursor.moveToNext()) { Preconditions.checkArgument( - iid == null, "Multiple iid found for " + "firebase app %s", appName); - iid = cursor.getString(cursor.getColumnIndex(INSTANCE_ID_COLUMN_NAME)); + value == null, "Multiple cache entries found for " + "firebase app %s", appName); + value = + CustomInstallationIdCacheEntryValue.create( + cursor.getString(cursor.getColumnIndex(CUSTOM_INSTALLATION_ID_COLUMN_NAME)), + cursor.getString(cursor.getColumnIndex(INSTANCE_ID_COLUMN_NAME)), + CacheStatus.values()[cursor.getInt(cursor.getColumnIndex(CACHE_STATUS_COLUMN))]); } - return iid; + return value; + } + + void insertOrUpdateCacheEntry( + FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { + String gmpAppId = firebaseApp.getOptions().getApplicationId(); + String appName = firebaseApp.getName(); + localDb.execSQL( + String.format( + "INSERT OR REPLACE INTO %s(%s, %s, %s, %s, %s) VALUES(%s, %s, %s, %s, %s)", + TABLE_NAME, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME, + CUSTOM_INSTALLATION_ID_COLUMN_NAME, + INSTANCE_ID_COLUMN_NAME, + CACHE_STATUS_COLUMN, + "\"" + gmpAppId + "\"", + "\"" + appName + "\"", + "\"" + entryValue.getCustomInstallationId() + "\"", + "\"" + entryValue.getFirebaseInstanceId() + "\"", + entryValue.getCacheStatus().ordinal())); + } + + @VisibleForTesting + void clear() { + localDb.execSQL(String.format("DROP TABLE IF EXISTS %s", TABLE_NAME)); } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java new file mode 100644 index 00000000000..c79d5f091a9 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java @@ -0,0 +1,37 @@ +// 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 com.google.auto.value.AutoValue; +import com.google.firebase.segmentation.CustomInstallationIdCache.CacheStatus; + +/** + * This class represents a cache entry value in {@link CustomInstallationIdCache}, which contains a + * Firebase instance id, a custom installation id and the cache status of this entry. + */ +@AutoValue +abstract class CustomInstallationIdCacheEntryValue { + abstract String getCustomInstallationId(); + + abstract String getFirebaseInstanceId(); + + abstract CacheStatus getCacheStatus(); + + static CustomInstallationIdCacheEntryValue create( + String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { + return new AutoValue_CustomInstallationIdCacheEntryValue( + customInstallationId, firebaseInstanceId, cacheStatus); + } +} From 0a3ebf6a5b2aa44d36469caf016e938e9376255b Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 14 Jun 2019 14:38:05 -0700 Subject: [PATCH 03/16] Add test for updating cache --- .../CustomInstallationIdCacheTest.java | 16 ++++++++++++---- .../segmentation/CustomInstallationIdCache.java | 6 ++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index c6b7ce0cecb..0dd24398e32 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -15,7 +15,6 @@ package com.google.firebase.segmentation; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import androidx.test.InstrumentationRegistry; @@ -66,12 +65,21 @@ public void testUpdateAndReadCacheEntry() { cache.insertOrUpdateCacheEntry( firebaseApp0, CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)); + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)); CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); - assertNotNull(entryValue); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); - assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); assertNull(cache.readCacheEntryValue(firebaseApp1)); + + cache.insertOrUpdateCacheEntry( + firebaseApp0, + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)); + entryValue = cache.readCacheEntryValue(firebaseApp0); + 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/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index afb1c86b5b0..94df707d3fe 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -109,8 +109,6 @@ CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) void insertOrUpdateCacheEntry( FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { - String gmpAppId = firebaseApp.getOptions().getApplicationId(); - String appName = firebaseApp.getName(); localDb.execSQL( String.format( "INSERT OR REPLACE INTO %s(%s, %s, %s, %s, %s) VALUES(%s, %s, %s, %s, %s)", @@ -120,8 +118,8 @@ void insertOrUpdateCacheEntry( CUSTOM_INSTALLATION_ID_COLUMN_NAME, INSTANCE_ID_COLUMN_NAME, CACHE_STATUS_COLUMN, - "\"" + gmpAppId + "\"", - "\"" + appName + "\"", + "\"" + firebaseApp.getOptions().getApplicationId() + "\"", + "\"" + firebaseApp.getName() + "\"", "\"" + entryValue.getCustomInstallationId() + "\"", "\"" + entryValue.getFirebaseInstanceId() + "\"", entryValue.getCacheStatus().ordinal())); From 2d158ed63a92b1130cca4f34286177de42e45e5e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 14 Jun 2019 17:03:52 -0700 Subject: [PATCH 04/16] Switch to use SQLiteOpenHelper --- .../firebase-segmentation.gradle | 2 +- .../CustomInstallationIdCache.java | 176 +++++++++++++----- 2 files changed, 127 insertions(+), 51 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index cc24fe30ced..dc4606715c5 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -57,7 +57,7 @@ android { compileSdkVersion project.targetSdkVersion defaultConfig { - minSdkVersion 21 + minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion multiDexEnabled true versionName version diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index 94df707d3fe..e2647f75cca 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -14,8 +14,11 @@ package com.google.firebase.segmentation; +import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; @@ -49,34 +52,105 @@ enum CacheStatus { String.format( "%s = ? " + "AND " + "%s = ?", GMP_APP_ID_COLUMN_NAME, FIREBASE_APP_NAME_COLUMN_NAME); - private final SQLiteDatabase localDb; + /** + * A SQLiteOpenHelper that configures database connections just the way we like them, delegating + * to SQLiteSchema to actually do the work of migration. + * + *

The order of events when opening a new connection is as follows: + * + *

    + *
  1. New connection + *
  2. onConfigure (API 16 and above) + *
  3. onCreate / onUpgrade (optional; if version already matches these aren't called) + *
  4. onOpen + *
+ * + *

This OpenHelper attempts to obtain exclusive access to the database and attempts to do so as + * early as possible. On Jelly Bean devices and above (some 98% of devices at time of writing) + * this happens naturally during onConfigure. On pre-Jelly Bean devices all other methods ensure + * that the configuration is applied before any action is taken. + */ + private static class OpenHelper extends SQLiteOpenHelper { + // TODO: when we do schema upgrades in the future we need to make sure both downgrades and + // upgrades work as expected, e.g. `up+down+up` is equivalent to `up`. + private static int SCHEMA_VERSION = 1; + + private boolean configured = false; + + private OpenHelper(Context context) { + super(context, LOCAL_DB_NAME, null, SCHEMA_VERSION); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + // Note that this is only called automatically by the SQLiteOpenHelper base class on Jelly + // Bean and above. + configured = true; + + db.rawQuery("PRAGMA busy_timeout=0;", new String[0]).close(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + db.setForeignKeyConstraintsEnabled(true); + } + } + + private void ensureConfigured(SQLiteDatabase db) { + if (!configured) { + onConfigure(db); + } + } + + @Override + public void onCreate(SQLiteDatabase db) { + ensureConfigured(db); + // Create custom id mapping table. + db.execSQL( + String.format( + "CREATE TABLE IF NOT EXISTS %s(%s TEXT NOT NULL, %s TEXT NOT NULL, " + + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL, PRIMARY KEY (%s, %s));", + TABLE_NAME, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME, + CUSTOM_INSTALLATION_ID_COLUMN_NAME, + INSTANCE_ID_COLUMN_NAME, + CACHE_STATUS_COLUMN, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME)); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + ensureConfigured(db); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + ensureConfigured(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + ensureConfigured(db); + } + } + + private final OpenHelper openHelper; 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/open + // 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/open // the database. - localDb = - SQLiteDatabase.openOrCreateDatabase( - FirebaseApp.getInstance() - .getApplicationContext() - .getNoBackupFilesDir() - .getAbsolutePath() - + "/" - + LOCAL_DB_NAME, - null); - - localDb.execSQL( - String.format( - "CREATE TABLE IF NOT EXISTS %s(%s TEXT NOT NULL, %s TEXT NOT NULL, " - + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL, PRIMARY KEY (%s, %s));", - TABLE_NAME, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME, - CUSTOM_INSTALLATION_ID_COLUMN_NAME, - INSTANCE_ID_COLUMN_NAME, - CACHE_STATUS_COLUMN, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME)); + openHelper = new OpenHelper(FirebaseApp.getInstance().getApplicationContext()); + } + + private SQLiteDatabase getReadableDb() { + return openHelper.getReadableDatabase(); + } + + private SQLiteDatabase getWritableDb() { + return openHelper.getWritableDatabase(); } @Nullable @@ -84,16 +158,17 @@ CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) String gmpAppId = firebaseApp.getOptions().getApplicationId(); String appName = firebaseApp.getName(); Cursor cursor = - localDb.query( - TABLE_NAME, - new String[] { - CUSTOM_INSTALLATION_ID_COLUMN_NAME, INSTANCE_ID_COLUMN_NAME, CACHE_STATUS_COLUMN - }, - QUERY_WHERE_CLAUSE, - new String[] {gmpAppId, appName}, - null, - null, - null); + getReadableDb() + .query( + TABLE_NAME, + new String[] { + CUSTOM_INSTALLATION_ID_COLUMN_NAME, INSTANCE_ID_COLUMN_NAME, CACHE_STATUS_COLUMN + }, + QUERY_WHERE_CLAUSE, + new String[] {gmpAppId, appName}, + null, + null, + null); CustomInstallationIdCacheEntryValue value = null; while (cursor.moveToNext()) { Preconditions.checkArgument( @@ -109,24 +184,25 @@ CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) void insertOrUpdateCacheEntry( FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { - localDb.execSQL( - String.format( - "INSERT OR REPLACE INTO %s(%s, %s, %s, %s, %s) VALUES(%s, %s, %s, %s, %s)", - TABLE_NAME, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME, - CUSTOM_INSTALLATION_ID_COLUMN_NAME, - INSTANCE_ID_COLUMN_NAME, - CACHE_STATUS_COLUMN, - "\"" + firebaseApp.getOptions().getApplicationId() + "\"", - "\"" + firebaseApp.getName() + "\"", - "\"" + entryValue.getCustomInstallationId() + "\"", - "\"" + entryValue.getFirebaseInstanceId() + "\"", - entryValue.getCacheStatus().ordinal())); + getWritableDb() + .execSQL( + String.format( + "INSERT OR REPLACE INTO %s(%s, %s, %s, %s, %s) VALUES(%s, %s, %s, %s, %s)", + TABLE_NAME, + GMP_APP_ID_COLUMN_NAME, + FIREBASE_APP_NAME_COLUMN_NAME, + CUSTOM_INSTALLATION_ID_COLUMN_NAME, + INSTANCE_ID_COLUMN_NAME, + CACHE_STATUS_COLUMN, + "\"" + firebaseApp.getOptions().getApplicationId() + "\"", + "\"" + firebaseApp.getName() + "\"", + "\"" + entryValue.getCustomInstallationId() + "\"", + "\"" + entryValue.getFirebaseInstanceId() + "\"", + entryValue.getCacheStatus().ordinal())); } @VisibleForTesting void clear() { - localDb.execSQL(String.format("DROP TABLE IF EXISTS %s", TABLE_NAME)); + getWritableDb().execSQL(String.format("DELETE FROM %s", TABLE_NAME)); } } From f118d39bf6cef56330d37ce154afa246b3891269 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 17 Jun 2019 14:20:39 -0700 Subject: [PATCH 05/16] Switch to use SharedPreferences from SQLite. --- .../CustomInstallationIdCacheTest.java | 2 +- .../CustomInstallationIdCache.java | 207 +++++------------- 2 files changed, 51 insertions(+), 158 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 0dd24398e32..3e085e32a22 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -51,7 +51,7 @@ public void setUp() { @After public void cleanUp() { - cache.clear(); + cache.clearAll(); } @Test diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index e2647f75cca..cb50fb3891c 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -14,14 +14,10 @@ package com.google.firebase.segmentation; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.os.Build; +import android.content.SharedPreferences; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.common.util.Strings; import com.google.firebase.FirebaseApp; class CustomInstallationIdCache { @@ -36,173 +32,70 @@ enum CacheStatus { // errors. PENDING, // Cache entry is not accepted by Firebase backend. - ERROR + ERROR, } - private static final String LOCAL_DB_NAME = "CustomInstallationIdCache"; - private static final String TABLE_NAME = "InstallationIdMapping"; + private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; - private static final String GMP_APP_ID_COLUMN_NAME = "GmpAppId"; - private static final String FIREBASE_APP_NAME_COLUMN_NAME = "AppName"; - private static final String CUSTOM_INSTALLATION_ID_COLUMN_NAME = "Cid"; - private static final String INSTANCE_ID_COLUMN_NAME = "Iid"; - private static final String CACHE_STATUS_COLUMN = "Status"; + private static final String CUSTOM_INSTALLATION_ID_KEY = "Cid"; + private static final String INSTANCE_ID_KEY = "Iid"; + private static final String CACHE_STATUS_KEY = "Status"; - private static final String QUERY_WHERE_CLAUSE = - String.format( - "%s = ? " + "AND " + "%s = ?", GMP_APP_ID_COLUMN_NAME, FIREBASE_APP_NAME_COLUMN_NAME); - - /** - * A SQLiteOpenHelper that configures database connections just the way we like them, delegating - * to SQLiteSchema to actually do the work of migration. - * - *

The order of events when opening a new connection is as follows: - * - *

    - *
  1. New connection - *
  2. onConfigure (API 16 and above) - *
  3. onCreate / onUpgrade (optional; if version already matches these aren't called) - *
  4. onOpen - *
- * - *

This OpenHelper attempts to obtain exclusive access to the database and attempts to do so as - * early as possible. On Jelly Bean devices and above (some 98% of devices at time of writing) - * this happens naturally during onConfigure. On pre-Jelly Bean devices all other methods ensure - * that the configuration is applied before any action is taken. - */ - private static class OpenHelper extends SQLiteOpenHelper { - // TODO: when we do schema upgrades in the future we need to make sure both downgrades and - // upgrades work as expected, e.g. `up+down+up` is equivalent to `up`. - private static int SCHEMA_VERSION = 1; - - private boolean configured = false; - - private OpenHelper(Context context) { - super(context, LOCAL_DB_NAME, null, SCHEMA_VERSION); - } - - @Override - public void onConfigure(SQLiteDatabase db) { - // Note that this is only called automatically by the SQLiteOpenHelper base class on Jelly - // Bean and above. - configured = true; - - db.rawQuery("PRAGMA busy_timeout=0;", new String[0]).close(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - db.setForeignKeyConstraintsEnabled(true); - } - } - - private void ensureConfigured(SQLiteDatabase db) { - if (!configured) { - onConfigure(db); - } - } - - @Override - public void onCreate(SQLiteDatabase db) { - ensureConfigured(db); - // Create custom id mapping table. - db.execSQL( - String.format( - "CREATE TABLE IF NOT EXISTS %s(%s TEXT NOT NULL, %s TEXT NOT NULL, " - + "%s TEXT NOT NULL, %s TEXT NOT NULL, %s INTEGER NOT NULL, PRIMARY KEY (%s, %s));", - TABLE_NAME, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME, - CUSTOM_INSTALLATION_ID_COLUMN_NAME, - INSTANCE_ID_COLUMN_NAME, - CACHE_STATUS_COLUMN, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME)); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - ensureConfigured(db); - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - ensureConfigured(db); - } - - @Override - public void onOpen(SQLiteDatabase db) { - ensureConfigured(db); - } - } - - private final OpenHelper openHelper; + private final SharedPreferences prefs; 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/open - // the database. - openHelper = new OpenHelper(FirebaseApp.getInstance().getApplicationContext()); + // 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. + prefs = + FirebaseApp.getInstance() + .getApplicationContext() + .getSharedPreferences(SHARED_PREFS_NAME, 0); // private mode } - private SQLiteDatabase getReadableDb() { - return openHelper.getReadableDatabase(); + @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); + + if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) { + return null; + } + + return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } - private SQLiteDatabase getWritableDb() { - return openHelper.getWritableDatabase(); + synchronized void insertOrUpdateCacheEntry( + FirebaseApp firebaseApp, 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()); + editor.commit(); } - @Nullable - CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { - String gmpAppId = firebaseApp.getOptions().getApplicationId(); - String appName = firebaseApp.getName(); - Cursor cursor = - getReadableDb() - .query( - TABLE_NAME, - new String[] { - CUSTOM_INSTALLATION_ID_COLUMN_NAME, INSTANCE_ID_COLUMN_NAME, CACHE_STATUS_COLUMN - }, - QUERY_WHERE_CLAUSE, - new String[] {gmpAppId, appName}, - null, - null, - null); - CustomInstallationIdCacheEntryValue value = null; - while (cursor.moveToNext()) { - Preconditions.checkArgument( - value == null, "Multiple cache entries found for " + "firebase app %s", appName); - value = - CustomInstallationIdCacheEntryValue.create( - cursor.getString(cursor.getColumnIndex(CUSTOM_INSTALLATION_ID_COLUMN_NAME)), - cursor.getString(cursor.getColumnIndex(INSTANCE_ID_COLUMN_NAME)), - CacheStatus.values()[cursor.getInt(cursor.getColumnIndex(CACHE_STATUS_COLUMN))]); - } - return value; + synchronized void 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)); } - void insertOrUpdateCacheEntry( - FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { - getWritableDb() - .execSQL( - String.format( - "INSERT OR REPLACE INTO %s(%s, %s, %s, %s, %s) VALUES(%s, %s, %s, %s, %s)", - TABLE_NAME, - GMP_APP_ID_COLUMN_NAME, - FIREBASE_APP_NAME_COLUMN_NAME, - CUSTOM_INSTALLATION_ID_COLUMN_NAME, - INSTANCE_ID_COLUMN_NAME, - CACHE_STATUS_COLUMN, - "\"" + firebaseApp.getOptions().getApplicationId() + "\"", - "\"" + firebaseApp.getName() + "\"", - "\"" + entryValue.getCustomInstallationId() + "\"", - "\"" + entryValue.getFirebaseInstanceId() + "\"", - entryValue.getCacheStatus().ordinal())); + private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) { + return String.format("%s|%s", firebaseApp.getPersistenceKey(), key); } @VisibleForTesting - void clear() { - getWritableDb().execSQL(String.format("DELETE FROM %s", TABLE_NAME)); + synchronized void clearAll() { + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + editor.commit(); } } From 4da5d31f31d2160e6ed3702e72d305f04a90e0c5 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 17 Jun 2019 17:05:01 -0700 Subject: [PATCH 06/16] Change the cache class to be singleton --- .../segmentation/CustomInstallationIdCacheTest.java | 2 +- .../segmentation/CustomInstallationIdCache.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 3e085e32a22..2645ab1571f 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -46,7 +46,7 @@ public void setUp() { InstrumentationRegistry.getContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); - cache = new CustomInstallationIdCache(); + cache = CustomInstallationIdCache.getInstance(); } @After diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index cb50fb3891c..1e48ca6d6c9 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -41,9 +41,17 @@ 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 SharedPreferences prefs; - CustomInstallationIdCache() { + static 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. From d1ff0ec0bcd7b111ea67d18950f8c5f455a759e9 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 10:41:01 -0700 Subject: [PATCH 07/16] Wrap shared pref commit in a async task. --- .../CustomInstallationIdCacheTest.java | 28 +++++++------ .../CustomInstallationIdCache.java | 39 ++++++++++++++----- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 2645ab1571f..5783294cfa3 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -16,9 +16,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import androidx.test.InstrumentationRegistry; 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 org.junit.After; @@ -50,8 +52,8 @@ public void setUp() { } @After - public void cleanUp() { - cache.clearAll(); + public void cleanUp() throws Exception { + Tasks.await(cache.clearAll()); } @Test @@ -61,11 +63,13 @@ public void testReadCacheEntry_Null() { } @Test - public void testUpdateAndReadCacheEntry() { - cache.insertOrUpdateCacheEntry( - firebaseApp0, - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)); + public void testUpdateAndReadCacheEntry() throws Exception { + assertTrue( + Tasks.await( + cache.insertOrUpdateCacheEntry( + firebaseApp0, + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)))); CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); @@ -73,10 +77,12 @@ public void testUpdateAndReadCacheEntry() { .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); assertNull(cache.readCacheEntryValue(firebaseApp1)); - cache.insertOrUpdateCacheEntry( - firebaseApp0, - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)); + assertTrue( + Tasks.await( + cache.insertOrUpdateCacheEntry( + firebaseApp0, + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); entryValue = cache.readCacheEntryValue(firebaseApp0); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index 1e48ca6d6c9..1863b976d9d 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -14,11 +14,16 @@ package com.google.firebase.segmentation; +import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; 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 { @@ -42,6 +47,7 @@ enum CacheStatus { private static final String CACHE_STATUS_KEY = "Status"; private static CustomInstallationIdCache singleton = null; + private final Executor ioExecuter; private final SharedPreferences prefs; static CustomInstallationIdCache getInstance() { @@ -58,7 +64,9 @@ private CustomInstallationIdCache() { prefs = FirebaseApp.getInstance() .getApplicationContext() - .getSharedPreferences(SHARED_PREFS_NAME, 0); // private mode + .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + + ioExecuter = Executors.newFixedThreadPool(2); } @Nullable @@ -75,7 +83,7 @@ synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } - synchronized void insertOrUpdateCacheEntry( + synchronized Task insertOrUpdateCacheEntry( FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( @@ -86,24 +94,37 @@ synchronized void insertOrUpdateCacheEntry( editor.putInt( getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); - editor.commit(); + return commitSharedPreferencesEditAsync(editor); } - synchronized void clear(FirebaseApp firebaseApp) { + 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)); + return commitSharedPreferencesEditAsync(editor); + } + + @VisibleForTesting + synchronized Task clearAll() { + SharedPreferences.Editor editor = prefs.edit(); + editor.clear(); + return commitSharedPreferencesEditAsync(editor); } private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) { return String.format("%s|%s", firebaseApp.getPersistenceKey(), key); } - @VisibleForTesting - synchronized void clearAll() { - SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); - editor.commit(); + private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { + TaskCompletionSource result = new TaskCompletionSource(); + ioExecuter.execute( + new Runnable() { + @Override + public void run() { + result.setResult(editor.commit()); + } + }); + return result.getTask(); } } From 41fbfee9e518794f9de09ee7e47ec5716fc92ead Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 11:02:46 -0700 Subject: [PATCH 08/16] Address comments --- .../firebase/segmentation/CustomInstallationIdCache.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index 1863b976d9d..2a7fb54d1e7 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -17,6 +17,7 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.util.Strings; import com.google.android.gms.tasks.Task; @@ -50,7 +51,7 @@ enum CacheStatus { private final Executor ioExecuter; private final SharedPreferences prefs; - static CustomInstallationIdCache getInstance() { + synchronized static CustomInstallationIdCache getInstance() { if (singleton == null) { singleton = new CustomInstallationIdCache(); } @@ -105,7 +106,7 @@ synchronized Task clear(FirebaseApp firebaseApp) { return commitSharedPreferencesEditAsync(editor); } - @VisibleForTesting + @RestrictTo(RestrictTo.Scope.TESTS) synchronized Task clearAll() { SharedPreferences.Editor editor = prefs.edit(); editor.clear(); From 5fd2fa0d6f4b83e152cb9bceb1fa2467e74e58e0 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 11:57:08 -0700 Subject: [PATCH 09/16] Google format fix --- .../firebase/segmentation/CustomInstallationIdCache.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java index 2a7fb54d1e7..5096a265714 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -18,7 +18,6 @@ import android.content.SharedPreferences; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; -import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.util.Strings; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -51,7 +50,7 @@ enum CacheStatus { private final Executor ioExecuter; private final SharedPreferences prefs; - synchronized static CustomInstallationIdCache getInstance() { + static synchronized CustomInstallationIdCache getInstance() { if (singleton == null) { singleton = new CustomInstallationIdCache(); } From dba0c0eb57aa655b60ebe88de91b121835fd9e00 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 15:16:35 -0700 Subject: [PATCH 10/16] Replace some deprecated code. --- .../segmentation/CustomInstallationIdCacheTest.java | 6 +++--- .../segmentation/FirebaseSegmentationInstrumentedTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 5783294cfa3..ced06450c3c 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import androidx.test.InstrumentationRegistry; +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; @@ -41,11 +41,11 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp0 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); firebaseApp1 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); cache = CustomInstallationIdCache.getInstance(); 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 db8fcb9b873..739df092564 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 @@ -16,7 +16,7 @@ import static org.junit.Assert.assertNull; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -39,7 +39,7 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); } From a9a43a45ac18db388618156c962fc55eeda75424 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:02:04 -0700 Subject: [PATCH 11/16] Package refactor --- .../{ => local}/CustomInstallationIdCacheTest.java | 6 ++++-- .../segmentation/{ => local}/CustomInstallationIdCache.java | 2 +- .../{ => local}/CustomInstallationIdCacheEntryValue.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) rename firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheTest.java (95%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCache.java (99%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheEntryValue.java (90%) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java similarity index 95% rename from firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java rename to firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index ced06450c3c..4a63177ba26 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; @@ -28,7 +28,9 @@ import org.junit.Test; import org.junit.runner.RunWith; -/** Instrumented tests for {@link CustomInstallationIdCache} */ +/** + * Instrumented tests for {@link com.google.firebase.segmentation.local.CustomInstallationIdCache} + */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java similarity index 99% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 5096a265714..2b93231eb39 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import android.content.Context; import android.content.SharedPreferences; diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java similarity index 90% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index c79d5f091a9..2d3b5f3c3a6 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import com.google.auto.value.AutoValue; -import com.google.firebase.segmentation.CustomInstallationIdCache.CacheStatus; +import com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus; /** * This class represents a cache entry value in {@link CustomInstallationIdCache}, which contains a From ca6dacfff2f9d9b29b0495b09927bf374892b96a Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:04:34 -0700 Subject: [PATCH 12/16] nit --- .../segmentation/local/CustomInstallationIdCacheTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4a63177ba26..b7028f1e595 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 @@ -29,7 +29,7 @@ import org.junit.runner.RunWith; /** - * Instrumented tests for {@link com.google.firebase.segmentation.local.CustomInstallationIdCache} + * Instrumented tests for {@link CustomInstallationIdCache} */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { From e7fff8152e3e912821622703565f8741497b6b9e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:05:32 -0700 Subject: [PATCH 13/16] nit --- .../segmentation/local/CustomInstallationIdCacheTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 b7028f1e595..27ab706b3a8 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 @@ -28,9 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -/** - * Instrumented tests for {@link CustomInstallationIdCache} - */ +/** Instrumented tests for {@link CustomInstallationIdCache} */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { From b381889c8ef8e86c0f8f662538421ce1763aba5b Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 19 Jun 2019 16:47:20 -0700 Subject: [PATCH 14/16] Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) --- .../firebase-segmentation.gradle | 7 + .../FirebaseSegmentationInstrumentedTest.java | 189 +++++++++++++++++- .../local/CustomInstallationIdCacheTest.java | 31 +-- .../segmentation/FirebaseSegmentation.java | 184 ++++++++++++++++- .../SetCustomInstallationIdException.java | 68 +++++++ .../google/firebase/segmentation/Utils.java | 33 +++ .../local/CustomInstallationIdCache.java | 87 ++++---- .../CustomInstallationIdCacheEntryValue.java | 10 +- .../remote/SegmentationServiceClient.java | 56 ++++++ .../FirebaseSegmentationRegistrarTest.java | 3 - 10 files changed, 588 insertions(+), 80 deletions(-) create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java 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..289c448fd1f 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,185 @@ * @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()); + 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..5bed0660b8a 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; } /** @@ -51,11 +75,165 @@ public static FirebaseSegmentation getInstance() { */ @NonNull public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { - Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + Preconditions.checkArgument(app != null, "Null is not a valid value " + "of FirebaseApp."); return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(String customInstallationId) { - return Tasks.forResult(null); + 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..f650c3463d8 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,78 +49,58 @@ 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(); + TaskCompletionSource result = new TaskCompletionSource<>(); ioExecuter.execute( new Runnable() { @Override 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()); } } From 1adcfbd864f3ed65f0898034bea59fefb0864156 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 19 Jun 2019 17:02:20 -0700 Subject: [PATCH 15/16] minor format fix --- .../segmentation/FirebaseSegmentationInstrumentedTest.java | 4 +--- .../google/firebase/segmentation/FirebaseSegmentation.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) 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 289c448fd1f..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 @@ -69,9 +69,7 @@ public void setUp() { firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder() - .setApplicationId("1" + ":123456789:android:abcdef") - .build()); + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); actualCache = new CustomInstallationIdCache(firebaseApp); when(backendClientReturnsOk.updateCustomInstallationId( 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 5bed0660b8a..e7566cd965b 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 @@ -75,7 +75,7 @@ public static FirebaseSegmentation getInstance() { */ @NonNull public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { - Preconditions.checkArgument(app != null, "Null is not a valid value " + "of FirebaseApp."); + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); return app.get(FirebaseSegmentation.class); } From 6091f82032fc78512d64de8e9e5c2257b98cdeda Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 20 Jun 2019 13:46:10 -0700 Subject: [PATCH 16/16] Address comments #1 --- .../firebase/segmentation/FirebaseSegmentation.java | 3 ++- .../segmentation/local/CustomInstallationIdCache.java | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) 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 e7566cd965b..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 @@ -79,7 +79,8 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(@Nullable String customInstallationId) { + @NonNull + public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { if (customInstallationId == null) { return clearCustomInstallationId(); } 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 f650c3463d8..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 @@ -101,13 +101,7 @@ private String getSharedPreferencesKey(String key) { private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { TaskCompletionSource result = new TaskCompletionSource<>(); - ioExecuter.execute( - new Runnable() { - @Override - public void run() { - result.setResult(editor.commit()); - } - }); + ioExecuter.execute(() -> result.setResult(editor.commit())); return result.getTask(); } }