Date: Fri, 14 Jun 2019 17:03:52 -0700
Subject: [PATCH 05/74] 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:
+ *
+ *
+ * - New connection
+ *
- onConfigure (API 16 and above)
+ *
- onCreate / onUpgrade (optional; if version already matches these aren't called)
+ *
- 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 06/74] 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:
- *
- *
- * - New connection
- *
- onConfigure (API 16 and above)
- *
- onCreate / onUpgrade (optional; if version already matches these aren't called)
- *
- 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 07/74] 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 08/74] 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 09/74] 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 10/74] 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 dc46eee03af6193666fd5b9d6e981eecfce65ccc Mon Sep 17 00:00:00 2001
From: Di Wu <49409954+diwu-arete@users.noreply.github.com>
Date: Tue, 18 Jun 2019 14:03:06 -0700
Subject: [PATCH 11/74] [Firebase Segmentation] Add custom installation id
cache layer and tests for it. (#524)
* Add type arguments in StorageTaskManager (#517)
* Output artifact list during local publishing. (#515)
This effort replaces #494.
* Implement Firebase segmentation SDK device local cache
* fix functions (#523)
* fix functions
* update minsdk version
* remove idea
* Set test type to release only in CI. (#522)
* Set test type to release only in CI.
This fixes Android Studio issue, where it is impossible to run
integration tests in debug mode.
Additionally move build type configuration to FirebaseLibraryPlugin to
avoid projects.all configuration in gradle.
* Add comment back.
* [Firebase Segmentation] Add custom installation id cache layer and tests for it.
* Add test for updating cache
* Switch to use SQLiteOpenHelper
* Minor fix to error message to match the admin sdk. (#525)
* Minor fix to error message to match the admin sdk.
In particular, it *is* allowed to have slashes, etc in field paths.
* Added clean task to smoke tests. (#527)
This change allows the smoke tests to clean all build variants created
by the infrastructure.
* Update deps to post-androidx gms versions. (#526)
* Update deps to post-androidx gms versions.
Additionally configure sources.jar for SDKs.
* Update functions-ktx deps
* Fix versions.
* unbump fiam version in fiamui-app
* Switch to use SharedPreferences from SQLite.
* Change the cache class to be singleton
* Copy firebase-firestore-ktx dependencies on firestore into its own subfolder (#528)
* Wrap shared pref commit in a async task.
* Address comments
* Bump firestore version for release (#530)
Additionally fix pom filter to exclude multidex from deps.
* Google format fix
---
buildSrc/build.gradle | 2 +-
.../gradle/plugins/FirebaseLibraryPlugin.java | 23 +-
.../plugins/ci/AffectedProjectFinder.groovy | 11 +
.../ci/ContinuousIntegrationPlugin.groovy | 8 -
.../gradle/plugins/ci/SmokeTestsPlugin.groovy | 105 +++
.../gradle/plugins/publish/Publisher.groovy | 2 +-
fiamui-app/fiamui-app.gradle | 6 +-
firebase-common/firebase-common.gradle | 4 +-
firebase-common/gradle.properties | 4 +-
.../firebase-database-collection.gradle | 2 +-
.../gradle.properties | 4 +-
firebase-database/firebase-database.gradle | 8 +-
firebase-database/gradle.properties | 4 +-
firebase-datatransport/gradle.properties | 4 +-
firebase-firestore/firebase-firestore.gradle | 8 +-
firebase-firestore/gradle.properties | 4 +-
firebase-firestore/ktx/ktx.gradle | 12 +-
.../firebase/firestore/TestAccessHelper.java | 31 +
.../google/firebase/firestore/TestUtil.java | 179 +++++
.../firebase/firestore/testutil/TestUtil.java | 618 ++++++++++++++++++
.../firebase/firestore/ValidationTest.java | 6 +-
.../google/firebase/firestore/FieldPath.java | 3 +-
firebase-functions/firebase-functions.gradle | 12 +-
firebase-functions/gradle.properties | 4 +-
firebase-functions/ktx/ktx.gradle | 4 +-
.../ktx/src/androidTest/AndroidManifest.xml | 2 +-
.../ktx/src/main/AndroidManifest.xml | 2 +-
.../src/main/AndroidManifest.xml | 2 +-
.../firebase-segmentation.gradle | 11 +-
.../CustomInstallationIdCacheTest.java | 91 +++
.../CustomInstallationIdCache.java | 130 ++++
.../CustomInstallationIdCacheEntryValue.java | 37 ++
firebase-storage/firebase-storage.gradle | 7 +-
firebase-storage/gradle.properties | 4 +-
.../firebase/storage/StorageTaskManager.java | 16 +-
firebase-storage/test-app/test-app.gradle | 8 +-
protolite-well-known-types/gradle.properties | 4 +-
root-project.gradle | 33 +-
smoke-tests/build.gradle | 12 +
.../apksize/src/firestore/firestore.gradle | 2 +-
transport/transport-api/gradle.properties | 4 +-
.../transport-backend-cct/gradle.properties | 4 +-
transport/transport-runtime/gradle.properties | 4 +-
43 files changed, 1320 insertions(+), 121 deletions(-)
create mode 100644 buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/SmokeTestsPlugin.groovy
create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java
create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java
create mode 100644 firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java
create mode 100644 firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java
create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java
create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 9cea827df36..a70e1c40638 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -37,6 +37,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.11.2'
implementation 'digital.wup:android-maven-publish:3.6.2'
implementation 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20'
+ implementation 'org.json:json:20180813'
implementation 'io.opencensus:opencensus-api:0.18.0'
implementation 'io.opencensus:opencensus-exporter-stats-stackdriver:0.18.0'
@@ -44,7 +45,6 @@ dependencies {
implementation 'com.android.tools.build:gradle:3.2.1'
testImplementation 'junit:junit:4.12'
- testImplementation 'org.json:json:20180813'
testImplementation('org.spockframework:spock-core:1.1-groovy-2.4') {
exclude group: 'org.codehaus.groovy'
}
diff --git a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java
index 5950a5477dd..522065eab14 100644
--- a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java
+++ b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java
@@ -20,7 +20,6 @@
import com.google.firebase.gradle.plugins.ci.device.FirebaseTestServer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
-import org.gradle.api.tasks.bundling.Jar;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
public class FirebaseLibraryPlugin implements Plugin {
@@ -33,6 +32,28 @@ public void apply(Project project) {
LibraryExtension android = project.getExtensions().getByType(LibraryExtension.class);
+ // In the case of and android library signing config only affects instrumentation test APK.
+ // We need it signed with default debug credentials in order for FTL to accept the APK.
+ android.buildTypes(
+ types ->
+ types
+ .getByName("release")
+ .setSigningConfig(types.getByName("debug").getSigningConfig()));
+
+ // skip debug tests in CI
+ // TODO(vkryachko): provide ability for teams to control this if needed
+ if (System.getenv().containsKey("FIREBASE_CI")) {
+ android.setTestBuildType("release");
+ project
+ .getTasks()
+ .all(
+ task -> {
+ if ("testDebugUnitTest".equals(task.getName())) {
+ task.setEnabled(false);
+ }
+ });
+ }
+
android.testServer(new FirebaseTestServer(project, firebaseLibrary.testLab));
// reduce the likelihood of kotlin module files colliding.
diff --git a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/AffectedProjectFinder.groovy b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/AffectedProjectFinder.groovy
index d04607189e9..ffe1e43099c 100644
--- a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/AffectedProjectFinder.groovy
+++ b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/AffectedProjectFinder.groovy
@@ -25,6 +25,10 @@ class AffectedProjectFinder {
Set changedPaths;
@Builder
+ AffectedProjectFinder(Project project, List ignorePaths) {
+ this(project, changedPaths(project.rootDir), ignorePaths)
+ }
+
AffectedProjectFinder(Project project,
Set changedPaths,
List ignorePaths) {
@@ -49,6 +53,13 @@ class AffectedProjectFinder {
return project.subprojects
}
+ private static Set changedPaths(File workDir) {
+ return 'git diff --name-only --submodule=diff HEAD@{0} HEAD@{1}'
+ .execute([], workDir)
+ .text
+ .readLines()
+ }
+
/**
* Performs a post-order project tree traversal and returns a set of projects that own the
* 'changedPaths'.
diff --git a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/ContinuousIntegrationPlugin.groovy b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/ContinuousIntegrationPlugin.groovy
index 95334ba6dbd..7c44748e6be 100644
--- a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/ContinuousIntegrationPlugin.groovy
+++ b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/ContinuousIntegrationPlugin.groovy
@@ -97,7 +97,6 @@ class ContinuousIntegrationPlugin implements Plugin {
def affectedProjects = AffectedProjectFinder.builder()
.project(project)
- .changedPaths(changedPaths(project.rootDir))
.ignorePaths(extension.ignorePaths)
.build()
.find()
@@ -143,13 +142,6 @@ class ContinuousIntegrationPlugin implements Plugin {
}
}
- private static Set changedPaths(File workDir) {
- return 'git diff --name-only --submodule=diff HEAD@{0} HEAD@{1}'
- .execute([], workDir)
- .text
- .readLines()
- }
-
private static final ANDROID_PLUGINS = ["com.android.application", "com.android.library",
"com.android.test"]
diff --git a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/SmokeTestsPlugin.groovy b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/SmokeTestsPlugin.groovy
new file mode 100644
index 00000000000..853e845418d
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/ci/SmokeTestsPlugin.groovy
@@ -0,0 +1,105 @@
+// Copyright 2018 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.gradle.plugins.ci
+
+import com.google.firebase.gradle.plugins.FirebaseLibraryExtension
+import com.google.firebase.gradle.plugins.ci.AffectedProjectFinder
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ProjectDependency
+import org.json.JSONArray
+import org.json.JSONObject
+
+/** Builds Firebase libraries for consumption by the smoke tests. */
+class SmokeTestsPlugin implements Plugin {
+ @Override
+ public void apply(Project project) {
+ def assembleAllTask = project.task("assembleAllForSmokeTests")
+
+ // Wait until after the projects have been evaluated or else we might skip projects.
+ project.gradle.projectsEvaluated {
+ def changedProjects = getChangedProjects(project)
+ def changedArtifacts = new HashSet()
+ def allArtifacts = new HashSet()
+
+ // Visit each project and add the artifacts to the appropriate sets.
+ project.subprojects {
+ def firebaseLibrary = it.extensions.findByType(FirebaseLibraryExtension)
+ if (firebaseLibrary == null) {
+ return
+ }
+
+ def groupId = firebaseLibrary.groupId.get()
+ def artifactId = firebaseLibrary.artifactId.get()
+ def artifact = "$groupId:$artifactId:$it.version-SNAPSHOT"
+ allArtifacts.add(artifact)
+
+ if (changedProjects.contains(it)) {
+ changedArtifacts.add(artifact)
+ }
+ }
+
+ // Reuse the publish task for building the libraries.
+ def publishAllTask = project.tasks.getByPath("publishAllToBuildDir")
+ assembleAllTask.dependsOn(publishAllTask)
+
+ // Generate a JSON file listing the artifacts after everything is complete.
+ assembleAllTask.doLast {
+ def changed = new JSONArray()
+ changedArtifacts.each { changed.put(it) }
+
+ def all = new JSONArray()
+ allArtifacts.each { all.put(it) }
+
+ def json = new JSONObject()
+ json.put("all", all)
+ json.put("changed", changed)
+
+ def path = project.buildDir.toPath()
+ path.resolve("m2repository/changed-artifacts.json").write(json.toString())
+ }
+ }
+ }
+
+ private static Set getChangedProjects(Project p) {
+ Set roots = new AffectedProjectFinder(p, []).find()
+ HashSet changed = new HashSet<>()
+
+ getChangedProjectsLoop(roots, changed)
+ return changed
+ }
+
+ private static void getChangedProjectsLoop(Collection projects, Set changed) {
+ for (Project p : projects) {
+ // Skip project if it is not a Firebase library.
+ if (p.extensions.findByType(FirebaseLibraryExtension) == null) {
+ continue;
+ }
+
+ // Skip processing and recursion if this project has already been added to the set.
+ if (!changed.add(p)) {
+ continue;
+ }
+
+ // Find all (head) dependencies to other projects in this respository.
+ def all = p.configurations.releaseRuntimeClasspath.allDependencies
+ def affected =
+ all.findAll { it instanceof ProjectDependency }.collect { it.getDependencyProject() }
+
+ // Recurse with the new dependencies.
+ getChangedProjectsLoop(affected, changed)
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/publish/Publisher.groovy b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/publish/Publisher.groovy
index 51036b38cc9..311e1626368 100644
--- a/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/publish/Publisher.groovy
+++ b/buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/publish/Publisher.groovy
@@ -74,7 +74,7 @@ class Publisher {
pom.dependencies.dependency.each {
// remove multidex as it is supposed to be added by final applications and is needed for
// some libraries only for instrumentation tests to build.
- if (it.groupId.text() in ['com.android.support', 'androidx'] && it.artifactId.text() == 'multidex') {
+ if (it.groupId.text() in ['com.android.support', 'androidx.multidex'] && it.artifactId.text() == 'multidex') {
it.parent().remove(it)
}
it.appendNode('type', [:], deps["${it.groupId.text()}:${it.artifactId.text()}"])
diff --git a/fiamui-app/fiamui-app.gradle b/fiamui-app/fiamui-app.gradle
index 4b3b3659e8e..3382bd3ce27 100644
--- a/fiamui-app/fiamui-app.gradle
+++ b/fiamui-app/fiamui-app.gradle
@@ -53,11 +53,11 @@ android {
dependencies {
implementation project(path: ":firebase-inappmessaging-display")
- implementation "com.google.firebase:firebase-measurement-connector:17.0.1"
+ implementation "com.google.firebase:firebase-measurement-connector:18.0.0"
implementation('com.google.firebase:firebase-inappmessaging:17.0.3') {
exclude group: 'com.google.firebase', module: 'firebase-common'
}
- implementation('com.google.firebase:firebase-analytics:16.0.4') {
+ implementation('com.google.firebase:firebase-analytics:17.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-common'
}
@@ -67,7 +67,7 @@ dependencies {
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "com.squareup.okhttp:okhttp:2.7.5"
implementation "com.google.auto.value:auto-value-annotations:1.6.5"
- implementation "com.google.android.gms:play-services-basement:16.2.0"
+ implementation "com.google.android.gms:play-services-basement:17.0.0"
// The following dependencies are not required to use the FIAM UI library.
// They are used to make some aspects of the demo app implementation simpler for
diff --git a/firebase-common/firebase-common.gradle b/firebase-common/firebase-common.gradle
index ed68cd80597..2a8c84ce1e6 100644
--- a/firebase-common/firebase-common.gradle
+++ b/firebase-common/firebase-common.gradle
@@ -58,8 +58,8 @@ android {
}
dependencies {
- implementation 'com.google.android.gms:play-services-basement:16.2.0'
- implementation "com.google.android.gms:play-services-tasks:16.0.1"
+ implementation 'com.google.android.gms:play-services-basement:17.0.0'
+ implementation "com.google.android.gms:play-services-tasks:17.0.0"
api 'com.google.auto.value:auto-value-annotations:1.6.5'
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
diff --git a/firebase-common/gradle.properties b/firebase-common/gradle.properties
index 5328ce212de..9b7be4891d1 100644
--- a/firebase-common/gradle.properties
+++ b/firebase-common/gradle.properties
@@ -1,2 +1,2 @@
-version=17.1.1
-latestReleasedVersion=17.1.0
+version=18.0.1
+latestReleasedVersion=18.0.0
diff --git a/firebase-database-collection/firebase-database-collection.gradle b/firebase-database-collection/firebase-database-collection.gradle
index 1d29321dd24..a9d5af2f3c3 100644
--- a/firebase-database-collection/firebase-database-collection.gradle
+++ b/firebase-database-collection/firebase-database-collection.gradle
@@ -29,7 +29,7 @@ android {
}
dependencies {
- implementation 'com.google.android.gms:play-services-base:16.1.0'
+ implementation 'com.google.android.gms:play-services-base:17.0.0'
testImplementation 'junit:junit:4.12'
testImplementation 'net.java:quickcheck:0.6'
diff --git a/firebase-database-collection/gradle.properties b/firebase-database-collection/gradle.properties
index c763f64467b..54be3eb478f 100644
--- a/firebase-database-collection/gradle.properties
+++ b/firebase-database-collection/gradle.properties
@@ -1,2 +1,2 @@
-version=16.0.2
-latestReleasedVersion=16.0.1
+version=17.0.1
+latestReleasedVersion=17.0.0
diff --git a/firebase-database/firebase-database.gradle b/firebase-database/firebase-database.gradle
index b6ef29308c9..b68e911ad69 100644
--- a/firebase-database/firebase-database.gradle
+++ b/firebase-database/firebase-database.gradle
@@ -73,10 +73,10 @@ dependencies {
implementation project(':firebase-common')
implementation project(':firebase-database-collection')
- implementation 'com.google.android.gms:play-services-basement:16.2.0'
- implementation 'com.google.android.gms:play-services-base:16.1.0'
- implementation 'com.google.android.gms:play-services-tasks:16.0.1'
- implementation('com.google.firebase:firebase-auth-interop:17.0.0') {
+ implementation 'com.google.android.gms:play-services-basement:17.0.0'
+ implementation 'com.google.android.gms:play-services-base:17.0.0'
+ implementation 'com.google.android.gms:play-services-tasks:17.0.0'
+ implementation('com.google.firebase:firebase-auth-interop:18.0.0') {
exclude group: "com.google.firebase", module: "firebase-common"
}
diff --git a/firebase-database/gradle.properties b/firebase-database/gradle.properties
index f4ae1a57594..b2337aeb5ba 100644
--- a/firebase-database/gradle.properties
+++ b/firebase-database/gradle.properties
@@ -12,6 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-version=17.0.0
-latestReleasedVersion=16.1.0
+version=18.0.1
+latestReleasedVersion=18.0.0
android.enableUnitTestBinaryResources=true
diff --git a/firebase-datatransport/gradle.properties b/firebase-datatransport/gradle.properties
index 03f6ea19074..a8dce55d4ea 100644
--- a/firebase-datatransport/gradle.properties
+++ b/firebase-datatransport/gradle.properties
@@ -1,3 +1,3 @@
-version=16.0.1
-latestReleasedVersion=16.0.0
+version=17.0.1
+latestReleasedVersion=17.0.0
android.enableUnitTestBinaryResources=true
diff --git a/firebase-firestore/firebase-firestore.gradle b/firebase-firestore/firebase-firestore.gradle
index f92916e98a1..9d8d86a686f 100644
--- a/firebase-firestore/firebase-firestore.gradle
+++ b/firebase-firestore/firebase-firestore.gradle
@@ -106,13 +106,13 @@ dependencies {
implementation 'io.grpc:grpc-protobuf-lite:1.21.0'
implementation 'io.grpc:grpc-okhttp:1.21.0'
implementation 'io.grpc:grpc-android:1.21.0'
- implementation 'com.google.android.gms:play-services-basement:16.2.0'
- implementation 'com.google.android.gms:play-services-tasks:16.0.1'
- implementation 'com.google.android.gms:play-services-base:16.1.0'
+ implementation 'com.google.android.gms:play-services-basement:17.0.0'
+ implementation 'com.google.android.gms:play-services-tasks:17.0.0'
+ implementation 'com.google.android.gms:play-services-base:17.0.0'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
- implementation('com.google.firebase:firebase-auth-interop:17.0.0') {
+ implementation('com.google.firebase:firebase-auth-interop:18.0.0') {
exclude group: "com.google.firebase", module: "firebase-common"
}
diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties
index 04d7bc444aa..346e8670d44 100644
--- a/firebase-firestore/gradle.properties
+++ b/firebase-firestore/gradle.properties
@@ -1,2 +1,2 @@
-version=19.0.2
-latestReleasedVersion=19.0.1
+version=20.1.0
+latestReleasedVersion=20.0.0
diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle
index cad7aece2d9..ffbfb8f652f 100644
--- a/firebase-firestore/ktx/ktx.gradle
+++ b/firebase-firestore/ktx/ktx.gradle
@@ -19,10 +19,11 @@ plugins {
firebaseLibrary {
releaseWith project(':firebase-firestore')
+ publishSources = true
}
android {
- compileSdkVersion project.targetSdkVersion
+ compileSdkVersion 28
defaultConfig {
minSdkVersion project.minSdkVersion
multiDexEnabled true
@@ -33,21 +34,22 @@ android {
main.java.srcDirs += 'src/main/kotlin'
test.java {
srcDir 'src/test/kotlin'
- srcDir '../src/testUtil/java'
- srcDir '../src/roboUtil/java'
+ srcDir 'src/test/java'
}
}
testOptions.unitTests.includeAndroidResources = true
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
-
implementation project(':firebase-common')
implementation project(':firebase-common:ktx')
implementation project(':firebase-firestore')
implementation 'androidx.annotation:annotation:1.1.0'
-
testImplementation project(':firebase-database-collection')
testImplementation 'org.mockito:mockito-core:2.25.0'
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java
new file mode 100644
index 00000000000..bab88979493
--- /dev/null
+++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestAccessHelper.java
@@ -0,0 +1,31 @@
+// Copyright 2018 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.firestore;
+
+import com.google.firebase.firestore.model.DocumentKey;
+
+public final class TestAccessHelper {
+
+ /** Makes the DocumentReference constructor accessible. */
+ public static DocumentReference createDocumentReference(DocumentKey documentKey) {
+ // We can use null here because the tests only use this as a wrapper for documentKeys.
+ return new DocumentReference(documentKey, null);
+ }
+
+ /** Makes the getKey() method accessible. */
+ public static DocumentKey referenceKey(DocumentReference documentReference) {
+ return documentReference.getKey();
+ }
+}
diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java
new file mode 100644
index 00000000000..d2d032ea3ae
--- /dev/null
+++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/TestUtil.java
@@ -0,0 +1,179 @@
+// 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.firestore;
+
+import static com.google.firebase.firestore.testutil.TestUtil.doc;
+import static com.google.firebase.firestore.testutil.TestUtil.docSet;
+import static com.google.firebase.firestore.testutil.TestUtil.key;
+import static org.mockito.Mockito.mock;
+
+import androidx.annotation.Nullable;
+import com.google.android.gms.tasks.Task;
+import com.google.firebase.database.collection.ImmutableSortedSet;
+import com.google.firebase.firestore.core.DocumentViewChange;
+import com.google.firebase.firestore.core.DocumentViewChange.Type;
+import com.google.firebase.firestore.core.ViewSnapshot;
+import com.google.firebase.firestore.local.QueryData;
+import com.google.firebase.firestore.model.Document;
+import com.google.firebase.firestore.model.DocumentKey;
+import com.google.firebase.firestore.model.DocumentSet;
+import com.google.firebase.firestore.model.ResourcePath;
+import com.google.firebase.firestore.model.value.ObjectValue;
+import com.google.firebase.firestore.remote.WatchChangeAggregator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Assert;
+import org.robolectric.Robolectric;
+
+public class TestUtil {
+
+ private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class);
+
+ public static FirebaseFirestore firestore() {
+ return FIRESTORE;
+ }
+
+ public static CollectionReference collectionReference(String path) {
+ return new CollectionReference(ResourcePath.fromString(path), FIRESTORE);
+ }
+
+ public static DocumentReference documentReference(String path) {
+ return new DocumentReference(key(path), FIRESTORE);
+ }
+
+ public static DocumentSnapshot documentSnapshot(
+ String path, Map data, boolean isFromCache) {
+ if (data == null) {
+ return DocumentSnapshot.fromNoDocument(
+ FIRESTORE, key(path), isFromCache, /*hasPendingWrites=*/ false);
+ } else {
+ return DocumentSnapshot.fromDocument(
+ FIRESTORE, doc(path, 1L, data), isFromCache, /*hasPendingWrites=*/ false);
+ }
+ }
+
+ public static Query query(String path) {
+ return new Query(com.google.firebase.firestore.testutil.TestUtil.query(path), FIRESTORE);
+ }
+
+ /**
+ * A convenience method for creating a particular query snapshot for tests.
+ *
+ * @param path To be used in constructing the query.
+ * @param oldDocs Provides the prior set of documents in the QuerySnapshot. Each entry maps to a
+ * document, with the key being the document id, and the value being the document contents.
+ * @param docsToAdd Specifies data to be added into the query snapshot as of now. Each entry maps
+ * to a document, with the key being the document id, and the value being the document
+ * contents.
+ * @param isFromCache Whether the query snapshot is cache result.
+ * @return A query snapshot that consists of both sets of documents.
+ */
+ public static QuerySnapshot querySnapshot(
+ String path,
+ Map oldDocs,
+ Map docsToAdd,
+ boolean hasPendingWrites,
+ boolean isFromCache) {
+ DocumentSet oldDocuments = docSet(Document.keyComparator());
+ ImmutableSortedSet mutatedKeys = DocumentKey.emptyKeySet();
+ for (Map.Entry pair : oldDocs.entrySet()) {
+ String docKey = path + "/" + pair.getKey();
+ oldDocuments =
+ oldDocuments.add(
+ doc(
+ docKey,
+ 1L,
+ pair.getValue(),
+ hasPendingWrites
+ ? Document.DocumentState.SYNCED
+ : Document.DocumentState.LOCAL_MUTATIONS));
+
+ if (hasPendingWrites) {
+ mutatedKeys = mutatedKeys.insert(key(docKey));
+ }
+ }
+ DocumentSet newDocuments = docSet(Document.keyComparator());
+ List documentChanges = new ArrayList<>();
+ for (Map.Entry pair : docsToAdd.entrySet()) {
+ String docKey = path + "/" + pair.getKey();
+ Document docToAdd =
+ doc(
+ docKey,
+ 1L,
+ pair.getValue(),
+ hasPendingWrites
+ ? Document.DocumentState.SYNCED
+ : Document.DocumentState.LOCAL_MUTATIONS);
+ newDocuments = newDocuments.add(docToAdd);
+ documentChanges.add(DocumentViewChange.create(Type.ADDED, docToAdd));
+
+ if (hasPendingWrites) {
+ mutatedKeys = mutatedKeys.insert(key(docKey));
+ }
+ }
+ ViewSnapshot viewSnapshot =
+ new ViewSnapshot(
+ com.google.firebase.firestore.testutil.TestUtil.query(path),
+ newDocuments,
+ oldDocuments,
+ documentChanges,
+ isFromCache,
+ mutatedKeys,
+ true,
+ /* excludesMetadataChanges= */ false);
+ return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE);
+ }
+
+ /**
+ * An implementation of TargetMetadataProvider that provides controlled access to the
+ * `TargetMetadataProvider` callbacks. Any target accessed via these callbacks must be registered
+ * beforehand via `setSyncedKeys()`.
+ */
+ public static class TestTargetMetadataProvider
+ implements WatchChangeAggregator.TargetMetadataProvider {
+ final Map> syncedKeys = new HashMap<>();
+ final Map queryData = new HashMap<>();
+
+ @Override
+ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) {
+ return syncedKeys.get(targetId) != null
+ ? syncedKeys.get(targetId)
+ : DocumentKey.emptyKeySet();
+ }
+
+ @Nullable
+ @Override
+ public QueryData getQueryDataForTarget(int targetId) {
+ return queryData.get(targetId);
+ }
+
+ /** Sets or replaces the local state for the provided query data. */
+ public void setSyncedKeys(QueryData queryData, ImmutableSortedSet keys) {
+ this.queryData.put(queryData.getTargetId(), queryData);
+ this.syncedKeys.put(queryData.getTargetId(), keys);
+ }
+ }
+
+ public static T waitFor(Task task) {
+ if (!task.isComplete()) {
+ Robolectric.flushBackgroundThreadScheduler();
+ }
+ Assert.assertTrue(
+ "Expected task to be completed after background thread flush", task.isComplete());
+ return task.getResult();
+ }
+}
diff --git a/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java
new file mode 100644
index 00000000000..c2ce41b0d8a
--- /dev/null
+++ b/firebase-firestore/ktx/src/test/java/com/google/firebase/firestore/testutil/TestUtil.java
@@ -0,0 +1,618 @@
+// 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.firestore.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
+import androidx.annotation.NonNull;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.firebase.Timestamp;
+import com.google.firebase.database.collection.ImmutableSortedMap;
+import com.google.firebase.database.collection.ImmutableSortedSet;
+import com.google.firebase.firestore.Blob;
+import com.google.firebase.firestore.DocumentReference;
+import com.google.firebase.firestore.TestAccessHelper;
+import com.google.firebase.firestore.UserDataConverter;
+import com.google.firebase.firestore.core.Filter;
+import com.google.firebase.firestore.core.Filter.Operator;
+import com.google.firebase.firestore.core.OrderBy;
+import com.google.firebase.firestore.core.OrderBy.Direction;
+import com.google.firebase.firestore.core.Query;
+import com.google.firebase.firestore.core.UserData.ParsedUpdateData;
+import com.google.firebase.firestore.local.LocalViewChanges;
+import com.google.firebase.firestore.local.QueryData;
+import com.google.firebase.firestore.local.QueryPurpose;
+import com.google.firebase.firestore.model.DatabaseId;
+import com.google.firebase.firestore.model.Document;
+import com.google.firebase.firestore.model.DocumentKey;
+import com.google.firebase.firestore.model.DocumentSet;
+import com.google.firebase.firestore.model.FieldPath;
+import com.google.firebase.firestore.model.MaybeDocument;
+import com.google.firebase.firestore.model.NoDocument;
+import com.google.firebase.firestore.model.ResourcePath;
+import com.google.firebase.firestore.model.SnapshotVersion;
+import com.google.firebase.firestore.model.UnknownDocument;
+import com.google.firebase.firestore.model.mutation.DeleteMutation;
+import com.google.firebase.firestore.model.mutation.FieldMask;
+import com.google.firebase.firestore.model.mutation.FieldTransform;
+import com.google.firebase.firestore.model.mutation.MutationResult;
+import com.google.firebase.firestore.model.mutation.PatchMutation;
+import com.google.firebase.firestore.model.mutation.Precondition;
+import com.google.firebase.firestore.model.mutation.SetMutation;
+import com.google.firebase.firestore.model.mutation.TransformMutation;
+import com.google.firebase.firestore.model.value.FieldValue;
+import com.google.firebase.firestore.model.value.ObjectValue;
+import com.google.firebase.firestore.remote.RemoteEvent;
+import com.google.firebase.firestore.remote.TargetChange;
+import com.google.firebase.firestore.remote.WatchChange.DocumentChange;
+import com.google.firebase.firestore.remote.WatchChangeAggregator;
+import com.google.protobuf.ByteString;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import javax.annotation.Nullable;
+
+/** A set of utilities for tests */
+public class TestUtil {
+
+ /** A string sentinel that can be used with patchMutation() to mark a field for deletion. */
+ public static final String DELETE_SENTINEL = "";
+
+ public static final long ARBITRARY_SEQUENCE_NUMBER = 2;
+
+ @SuppressWarnings("unchecked")
+ public static Map map(Object... entries) {
+ Map res = new HashMap<>();
+ for (int i = 0; i < entries.length; i += 2) {
+ res.put((String) entries[i], (T) entries[i + 1]);
+ }
+ return res;
+ }
+
+ public static Blob blob(int... bytes) {
+ return Blob.fromByteString(byteString(bytes));
+ }
+
+ public static ByteString byteString(int... bytes) {
+ byte[] primitive = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ primitive[i] = (byte) bytes[i];
+ }
+ return ByteString.copyFrom(primitive);
+ }
+
+ public static FieldMask fieldMask(String... fields) {
+ FieldPath[] mask = new FieldPath[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ mask[i] = field(fields[i]);
+ }
+ return FieldMask.fromSet(new HashSet<>(Arrays.asList(mask)));
+ }
+
+ public static final Map EMPTY_MAP = new HashMap<>();
+
+ public static FieldValue wrap(Object value) {
+ DatabaseId databaseId = DatabaseId.forProject("project");
+ UserDataConverter dataConverter = new UserDataConverter(databaseId);
+ // HACK: We use parseQueryValue() since it accepts scalars as well as arrays / objects, and
+ // our tests currently use wrap() pretty generically so we don't know the intent.
+ return dataConverter.parseQueryValue(value);
+ }
+
+ public static ObjectValue wrapObject(Map value) {
+ // Cast is safe here because value passed in is a map
+ return (ObjectValue) wrap(value);
+ }
+
+ public static ObjectValue wrapObject(Object... entries) {
+ return wrapObject(map(entries));
+ }
+
+ public static DocumentKey key(String key) {
+ return DocumentKey.fromPathString(key);
+ }
+
+ public static ResourcePath path(String key) {
+ return ResourcePath.fromString(key);
+ }
+
+ public static Query query(String path) {
+ return Query.atPath(path(path));
+ }
+
+ public static FieldPath field(String path) {
+ return FieldPath.fromSegments(Arrays.asList(path.split("\\.")));
+ }
+
+ public static DocumentReference ref(String key) {
+ return TestAccessHelper.createDocumentReference(key(key));
+ }
+
+ public static DatabaseId dbId(String project, String database) {
+ return DatabaseId.forDatabase(project, database);
+ }
+
+ public static DatabaseId dbId(String project) {
+ return DatabaseId.forProject(project);
+ }
+
+ public static SnapshotVersion version(long versionMicros) {
+ long seconds = versionMicros / 1000000;
+ int nanos = (int) (versionMicros % 1000000L) * 1000;
+ return new SnapshotVersion(new Timestamp(seconds, nanos));
+ }
+
+ public static Document doc(String key, long version, Map data) {
+ return new Document(
+ key(key), version(version), wrapObject(data), Document.DocumentState.SYNCED);
+ }
+
+ public static Document doc(DocumentKey key, long version, Map data) {
+ return new Document(key, version(version), wrapObject(data), Document.DocumentState.SYNCED);
+ }
+
+ public static Document doc(
+ String key, long version, ObjectValue data, Document.DocumentState documentState) {
+ return new Document(key(key), version(version), data, documentState);
+ }
+
+ public static Document doc(
+ String key, long version, Map data, Document.DocumentState documentState) {
+ return new Document(key(key), version(version), wrapObject(data), documentState);
+ }
+
+ public static NoDocument deletedDoc(String key, long version) {
+ return deletedDoc(key, version, /*hasCommittedMutations=*/ false);
+ }
+
+ public static NoDocument deletedDoc(String key, long version, boolean hasCommittedMutations) {
+ return new NoDocument(key(key), version(version), hasCommittedMutations);
+ }
+
+ public static UnknownDocument unknownDoc(String key, long version) {
+ return new UnknownDocument(key(key), version(version));
+ }
+
+ public static DocumentSet docSet(Comparator comparator, Document... documents) {
+ DocumentSet set = DocumentSet.emptySet(comparator);
+ for (Document document : documents) {
+ set = set.add(document);
+ }
+ return set;
+ }
+
+ public static ImmutableSortedSet keySet(DocumentKey... keys) {
+ ImmutableSortedSet keySet = DocumentKey.emptyKeySet();
+ for (DocumentKey key : keys) {
+ keySet = keySet.insert(key);
+ }
+ return keySet;
+ }
+
+ public static Filter filter(String key, String operator, Object value) {
+ return Filter.create(field(key), operatorFromString(operator), wrap(value));
+ }
+
+ public static Operator operatorFromString(String s) {
+ if (s.equals("<")) {
+ return Operator.LESS_THAN;
+ } else if (s.equals("<=")) {
+ return Operator.LESS_THAN_OR_EQUAL;
+ } else if (s.equals("==")) {
+ return Operator.EQUAL;
+ } else if (s.equals(">")) {
+ return Operator.GREATER_THAN;
+ } else if (s.equals(">=")) {
+ return Operator.GREATER_THAN_OR_EQUAL;
+ } else if (s.equals("array-contains")) {
+ return Operator.ARRAY_CONTAINS;
+ } else {
+ throw new IllegalStateException("Unknown operator: " + s);
+ }
+ }
+
+ public static OrderBy orderBy(String key) {
+ return orderBy(key, "asc");
+ }
+
+ public static OrderBy orderBy(String key, String dir) {
+ Direction direction;
+ if (dir.equals("asc")) {
+ direction = Direction.ASCENDING;
+ } else if (dir.equals("desc")) {
+ direction = Direction.DESCENDING;
+ } else {
+ throw new IllegalArgumentException("Unknown direction: " + dir);
+ }
+ return OrderBy.getInstance(direction, field(key));
+ }
+
+ public static void testEquality(List> equalityGroups) {
+ for (int i = 0; i < equalityGroups.size(); i++) {
+ List> group = equalityGroups.get(i);
+ for (Object value : group) {
+ for (List> otherGroup : equalityGroups) {
+ for (Object otherValue : otherGroup) {
+ if (otherGroup == group) {
+ assertEquals(value, otherValue);
+ } else {
+ assertNotEquals(value, otherValue);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public static QueryData queryData(int targetId, QueryPurpose queryPurpose, String path) {
+ return new QueryData(query(path), targetId, ARBITRARY_SEQUENCE_NUMBER, queryPurpose);
+ }
+
+ public static ImmutableSortedMap docUpdates(MaybeDocument... docs) {
+ ImmutableSortedMap res =
+ ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator());
+ for (MaybeDocument doc : docs) {
+ res = res.insert(doc.getKey(), doc);
+ }
+ return res;
+ }
+
+ public static ImmutableSortedMap docUpdates(Document... docs) {
+ ImmutableSortedMap res =
+ ImmutableSortedMap.Builder.emptyMap(DocumentKey.comparator());
+ for (Document doc : docs) {
+ res = res.insert(doc.getKey(), doc);
+ }
+ return res;
+ }
+
+ public static TargetChange targetChange(
+ ByteString resumeToken,
+ boolean current,
+ @Nullable Collection addedDocuments,
+ @Nullable Collection modifiedDocuments,
+ @Nullable Collection extends MaybeDocument> removedDocuments) {
+ ImmutableSortedSet addedDocumentKeys = DocumentKey.emptyKeySet();
+ ImmutableSortedSet modifiedDocumentKeys = DocumentKey.emptyKeySet();
+ ImmutableSortedSet removedDocumentKeys = DocumentKey.emptyKeySet();
+
+ if (addedDocuments != null) {
+ for (Document document : addedDocuments) {
+ addedDocumentKeys = addedDocumentKeys.insert(document.getKey());
+ }
+ }
+
+ if (modifiedDocuments != null) {
+ for (Document document : modifiedDocuments) {
+ modifiedDocumentKeys = modifiedDocumentKeys.insert(document.getKey());
+ }
+ }
+
+ if (removedDocuments != null) {
+ for (MaybeDocument document : removedDocuments) {
+ removedDocumentKeys = removedDocumentKeys.insert(document.getKey());
+ }
+ }
+
+ return new TargetChange(
+ resumeToken, current, addedDocumentKeys, modifiedDocumentKeys, removedDocumentKeys);
+ }
+
+ public static TargetChange ackTarget(Document... docs) {
+ return targetChange(ByteString.EMPTY, true, Arrays.asList(docs), null, null);
+ }
+
+ public static Map activeQueries(Iterable targets) {
+ Query query = query("foo");
+ Map listenMap = new HashMap<>();
+ for (Integer targetId : targets) {
+ QueryData queryData =
+ new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LISTEN);
+ listenMap.put(targetId, queryData);
+ }
+ return listenMap;
+ }
+
+ public static Map activeQueries(Integer... targets) {
+ return activeQueries(asList(targets));
+ }
+
+ public static Map activeLimboQueries(
+ String docKey, Iterable targets) {
+ Query query = query(docKey);
+ Map listenMap = new HashMap<>();
+ for (Integer targetId : targets) {
+ QueryData queryData =
+ new QueryData(query, targetId, ARBITRARY_SEQUENCE_NUMBER, QueryPurpose.LIMBO_RESOLUTION);
+ listenMap.put(targetId, queryData);
+ }
+ return listenMap;
+ }
+
+ public static Map activeLimboQueries(String docKey, Integer... targets) {
+ return activeLimboQueries(docKey, asList(targets));
+ }
+
+ public static RemoteEvent addedRemoteEvent(
+ MaybeDocument doc, List updatedInTargets, List removedFromTargets) {
+ DocumentChange change =
+ new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc);
+ WatchChangeAggregator aggregator =
+ new WatchChangeAggregator(
+ new WatchChangeAggregator.TargetMetadataProvider() {
+ @Override
+ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) {
+ return DocumentKey.emptyKeySet();
+ }
+
+ @Override
+ public QueryData getQueryDataForTarget(int targetId) {
+ return queryData(targetId, QueryPurpose.LISTEN, doc.getKey().toString());
+ }
+ });
+ aggregator.handleDocumentChange(change);
+ return aggregator.createRemoteEvent(doc.getVersion());
+ }
+
+ public static RemoteEvent updateRemoteEvent(
+ MaybeDocument doc, List updatedInTargets, List removedFromTargets) {
+ return updateRemoteEvent(doc, updatedInTargets, removedFromTargets, Collections.emptyList());
+ }
+
+ public static RemoteEvent updateRemoteEvent(
+ MaybeDocument doc,
+ List updatedInTargets,
+ List removedFromTargets,
+ List limboTargets) {
+ DocumentChange change =
+ new DocumentChange(updatedInTargets, removedFromTargets, doc.getKey(), doc);
+ WatchChangeAggregator aggregator =
+ new WatchChangeAggregator(
+ new WatchChangeAggregator.TargetMetadataProvider() {
+ @Override
+ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) {
+ return DocumentKey.emptyKeySet().insert(doc.getKey());
+ }
+
+ @Override
+ public QueryData getQueryDataForTarget(int targetId) {
+ boolean isLimbo =
+ !(updatedInTargets.contains(targetId) || removedFromTargets.contains(targetId));
+ QueryPurpose purpose =
+ isLimbo ? QueryPurpose.LIMBO_RESOLUTION : QueryPurpose.LISTEN;
+ return queryData(targetId, purpose, doc.getKey().toString());
+ }
+ });
+ aggregator.handleDocumentChange(change);
+ return aggregator.createRemoteEvent(doc.getVersion());
+ }
+
+ public static SetMutation setMutation(String path, Map values) {
+ return new SetMutation(key(path), wrapObject(values), Precondition.NONE);
+ }
+
+ public static PatchMutation patchMutation(String path, Map values) {
+ return patchMutation(path, values, null);
+ }
+
+ public static PatchMutation patchMutation(
+ String path, Map values, @Nullable List updateMask) {
+ ObjectValue objectValue = ObjectValue.emptyObject();
+ ArrayList objectMask = new ArrayList<>();
+ for (Entry entry : values.entrySet()) {
+ FieldPath fieldPath = field(entry.getKey());
+ objectMask.add(fieldPath);
+ if (!entry.getValue().equals(DELETE_SENTINEL)) {
+ FieldValue parsedValue = wrap(entry.getValue());
+ objectValue = objectValue.set(fieldPath, parsedValue);
+ }
+ }
+
+ boolean merge = updateMask != null;
+
+ // We sort the fieldMaskPaths to make the order deterministic in tests. (Otherwise, when we
+ // flatten a Set to a proto repeated field, we'll end up comparing in iterator order and
+ // possibly consider {foo,bar} != {bar,foo}.)
+ SortedSet fieldMaskPaths = new TreeSet<>(merge ? updateMask : objectMask);
+
+ return new PatchMutation(
+ key(path),
+ objectValue,
+ FieldMask.fromSet(fieldMaskPaths),
+ merge ? Precondition.NONE : Precondition.exists(true));
+ }
+
+ public static DeleteMutation deleteMutation(String path) {
+ return new DeleteMutation(key(path), Precondition.NONE);
+ }
+
+ /**
+ * Creates a TransformMutation by parsing any FieldValue sentinels in the provided data. The data
+ * is expected to use dotted-notation for nested fields (i.e. { "foo.bar": FieldValue.foo() } and
+ * must not contain any non-sentinel data.
+ */
+ public static TransformMutation transformMutation(String path, Map data) {
+ UserDataConverter dataConverter = new UserDataConverter(DatabaseId.forProject("project"));
+ ParsedUpdateData result = dataConverter.parseUpdateData(data);
+
+ // The order of the transforms doesn't matter, but we sort them so tests can assume a particular
+ // order.
+ ArrayList fieldTransforms = new ArrayList<>(result.getFieldTransforms());
+ Collections.sort(
+ fieldTransforms, (ft1, ft2) -> ft1.getFieldPath().compareTo(ft2.getFieldPath()));
+
+ return new TransformMutation(key(path), fieldTransforms);
+ }
+
+ public static MutationResult mutationResult(long version) {
+ return new MutationResult(version(version), null);
+ }
+
+ public static LocalViewChanges viewChanges(
+ int targetId, List addedKeys, List removedKeys) {
+ ImmutableSortedSet added = DocumentKey.emptyKeySet();
+ for (String keyPath : addedKeys) {
+ added = added.insert(key(keyPath));
+ }
+ ImmutableSortedSet removed = DocumentKey.emptyKeySet();
+ for (String keyPath : removedKeys) {
+ removed = removed.insert(key(keyPath));
+ }
+ return new LocalViewChanges(targetId, added, removed);
+ }
+
+ /** Creates a resume token to match the given snapshot version. */
+ @Nullable
+ public static ByteString resumeToken(long snapshotVersion) {
+ if (snapshotVersion == 0) {
+ return null;
+ }
+
+ String snapshotString = "snapshot-" + snapshotVersion;
+ return ByteString.copyFrom(snapshotString, Charsets.UTF_8);
+ }
+
+ @NonNull
+ private static ByteString resumeToken(SnapshotVersion snapshotVersion) {
+ if (snapshotVersion.equals(SnapshotVersion.NONE)) {
+ return ByteString.EMPTY;
+ } else {
+ return ByteString.copyFromUtf8(snapshotVersion.toString());
+ }
+ }
+
+ public static ByteString streamToken(String contents) {
+ return ByteString.copyFrom(contents, Charsets.UTF_8);
+ }
+
+ private static Map fromJsonString(String json) {
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readValue(json, new TypeReference