From 8881325d71e3863b3ec89971b6254c3316017010 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 13 Jun 2019 13:54:46 -0700 Subject: [PATCH 01/10] 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/10] [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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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()); }