From 0d0528232c9f730a469086b40ad12bdfde7eb7a3 Mon Sep 17 00:00:00 2001 From: diwu-arete <49409954+diwu-arete@users.noreply.github.com> Date: Wed, 12 Jun 2019 17:49:19 -0700 Subject: [PATCH 01/74] Add Firebase Segmentation SDK and some skeleton code in Firebase Android SDK (#514) * Add Firebase Segmentation SDK and some skeleton code in Firebase Android SDK * Add Firebase Segmentation SDK and some skeleton code in Firebase Android SDK * Address comments #1 * Address comments #1 * Address comments #2 --- .../firebase-segmentation.gradle | 102 ++++++++++++++++++ firebase-segmentation/gradle.properties | 1 + firebase-segmentation/lint.xml | 11 ++ .../src/androidTest/AndroidManifest.xml | 26 +++++ .../FirebaseSegmentationInstrumentedTest.java | 50 +++++++++ .../src/main/AndroidManifest.xml | 28 +++++ .../segmentation/FirebaseSegmentation.java | 61 +++++++++++ .../FirebaseSegmentationRegistrar.java | 36 +++++++ .../FirebaseSegmentationRegistrarTest.java | 57 ++++++++++ subprojects.cfg | 1 + 10 files changed, 373 insertions(+) create mode 100644 firebase-segmentation/firebase-segmentation.gradle create mode 100644 firebase-segmentation/gradle.properties create mode 100644 firebase-segmentation/lint.xml create mode 100644 firebase-segmentation/src/androidTest/AndroidManifest.xml create mode 100644 firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java create mode 100644 firebase-segmentation/src/main/AndroidManifest.xml create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java create mode 100644 firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle new file mode 100644 index 00000000000..88ea12a1ed0 --- /dev/null +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -0,0 +1,102 @@ +// 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. + +plugins { + id 'firebase-library' + id 'com.google.protobuf' +} + +firebaseLibrary { + testLab.enabled = true +} + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = 'com.google.protobuf:protoc:3.4.0' + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.12.0' + } + javalite { + // The codegen for lite comes as a separate artifact + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + // In most cases you don't need the full Java output + // if you use the lite output. + remove java + } + task.plugins { + grpc { + option 'lite' + } + javalite {} + } + } + } +} + +android { + compileSdkVersion project.targetSdkVersion + + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + multiDexEnabled true + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + sourceSets { + main { + proto { + srcDir 'src/main/proto' + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':firebase-common') + + implementation('com.google.firebase:firebase-iid:17.0.3') { + exclude group: "com.google.firebase", module: "firebase-common" + } + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.multidex:multidex:2.0.0' + implementation 'com.google.android.gms:play-services-tasks:16.0.1' + + 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:runner:1.2.0' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' +} diff --git a/firebase-segmentation/gradle.properties b/firebase-segmentation/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-segmentation/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-segmentation/lint.xml b/firebase-segmentation/lint.xml new file mode 100644 index 00000000000..9c521180b8f --- /dev/null +++ b/firebase-segmentation/lint.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/firebase-segmentation/src/androidTest/AndroidManifest.xml b/firebase-segmentation/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000000..f3ec53d62a2 --- /dev/null +++ b/firebase-segmentation/src/androidTest/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000000..db8fcb9b873 --- /dev/null +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -0,0 +1,50 @@ +// 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 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.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class FirebaseSegmentationInstrumentedTest { + + private FirebaseApp firebaseApp; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp = + FirebaseApp.initializeApp( + InstrumentationRegistry.getContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + } + + @Test + public void useAppContext() { + assertNull(FirebaseSegmentation.getInstance().setCustomInstallationId("123123").getResult()); + } +} diff --git a/firebase-segmentation/src/main/AndroidManifest.xml b/firebase-segmentation/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..1502b31485f --- /dev/null +++ b/firebase-segmentation/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java new file mode 100644 index 00000000000..eca517db1b3 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -0,0 +1,61 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.iid.FirebaseInstanceId; + +/** Entry point of Firebase Segmentation SDK. */ +public class FirebaseSegmentation { + + private final FirebaseApp firebaseApp; + private final FirebaseInstanceId firebaseInstanceId; + + FirebaseSegmentation(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); + } + + /** + * Returns the {@link FirebaseSegmentation} initialized with the default {@link FirebaseApp}. + * + * @return a {@link FirebaseSegmentation} instance + */ + @NonNull + public static FirebaseSegmentation getInstance() { + FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); + return getInstance(defaultFirebaseApp); + } + + /** + * Returns the {@link FirebaseSegmentation} initialized with a custom {@link FirebaseApp}. + * + * @param app a custom {@link FirebaseApp} + * @return a {@link FirebaseSegmentation} instance + */ + @NonNull + public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + return app.get(FirebaseSegmentation.class); + } + + Task setCustomInstallationId(String customInstallationId) { + return Tasks.forResult(null); + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java new file mode 100644 index 00000000000..7d1d5fcfaab --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java @@ -0,0 +1,36 @@ +// 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.firebase.FirebaseApp; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +public class FirebaseSegmentationRegistrar implements ComponentRegistrar { + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseSegmentation.class) + .add(Dependency.required(FirebaseApp.class)) + .factory(c -> new FirebaseSegmentation(c.get(FirebaseApp.class))) + .build(), + LibraryVersionComponent.create("fire-segmentation", BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java new file mode 100644 index 00000000000..1f3c441808f --- /dev/null +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java @@ -0,0 +1,57 @@ +// 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 org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import androidx.test.core.app.ApplicationProvider; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class FirebaseSegmentationRegistrarTest { + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + } + + @Test + public void getFirebaseInstallationsInstance() { + FirebaseApp defaultApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + + FirebaseApp anotherApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + + FirebaseSegmentation defaultSegmentation = FirebaseSegmentation.getInstance(); + assertNotNull(defaultSegmentation); + assertNull(defaultSegmentation.setCustomInstallationId("12345").getResult()); + + FirebaseSegmentation anotherSegmentation = FirebaseSegmentation.getInstance(anotherApp); + assertNotNull(anotherSegmentation); + assertNull(anotherSegmentation.setCustomInstallationId("ghdjaas").getResult()); + } +} diff --git a/subprojects.cfg b/subprojects.cfg index 811bd1c0d99..f949f8c6b2a 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -11,6 +11,7 @@ firebase-datatransport fiamui-app firebase-storage firebase-storage:test-app +firebase-segmentation protolite-well-known-types transport From 8881325d71e3863b3ec89971b6254c3316017010 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 13 Jun 2019 13:54:46 -0700 Subject: [PATCH 02/74] 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 03/74] [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 04/74] 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 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: + * + *

    + *
  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 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: - * - *

    - *
  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 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 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>() {}); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Map fromSingleQuotedString(String json) { + return fromJsonString(json.replace("'", "\"")); + } + + /** Converts the values of an ImmutableSortedMap into a list, preserving key order. */ + public static List values(ImmutableSortedMap map) { + List result = new ArrayList<>(); + for (Map.Entry entry : map) { + result.add(entry.getValue()); + } + return result; + } + + /** + * Asserts that the actual set is equal to the expected one. + * + * @param expected A list of the expected contents of the set, in order. + * @param actual The set to compare against. + * @param The type of the values of in common between the expected list and actual set. + */ + // PORTING NOTE: JUnit and XCTest use reversed conventions on expected and actual values :-(. + public static void assertSetEquals(List expected, ImmutableSortedSet actual) { + List actualList = Lists.newArrayList(actual); + assertEquals(expected, actualList); + } + + /** + * Asserts that the actual set is equal to the expected one. + * + * @param expected A list of the expected contents of the set, in order. + * @param actual The set to compare against. + * @param The type of the values of in common between the expected list and actual set. + */ + // PORTING NOTE: JUnit and XCTest use reversed conventions on expected and actual values :-(. + public static void assertSetEquals(List expected, Set actual) { + Set expectedSet = Sets.newHashSet(expected); + assertEquals(expectedSet, actual); + } + + /** Asserts that the given runnable block fails with an internal error. */ + public static void assertFails(Runnable block) { + try { + block.run(); + } catch (AssertionError e) { + assertThat(e).hasMessageThat().startsWith("INTERNAL ASSERTION FAILED:"); + // Otherwise success + return; + } + fail("Should have failed"); + } + + public static void assertDoesNotThrow(Runnable block) { + try { + block.run(); + } catch (Exception e) { + fail("Should not have thrown " + e); + } + } + + // TODO: We could probably do some de-duplication between assertFails / expectError. + /** Expects runnable to throw an exception with a specific error message. */ + public static void expectError(Runnable runnable, String exceptionMessage) { + expectError(runnable, exceptionMessage, /*context=*/ null); + } + + /** + * Expects runnable to throw an exception with a specific error message. An optional context (e.g. + * "for bad_data") can be provided which will be displayed in any resulting failure message. + */ + public static void expectError(Runnable runnable, String exceptionMessage, String context) { + boolean exceptionThrown = false; + try { + runnable.run(); + } catch (Throwable throwable) { + exceptionThrown = true; + String contextMessage = "Expected exception message was incorrect"; + if (context != null) { + contextMessage += " (" + context + ")"; + } + assertEquals(contextMessage, exceptionMessage, throwable.getMessage()); + } + if (!exceptionThrown) { + context = (context == null) ? "" : context; + fail( + "Expected exception with message '" + + exceptionMessage + + "' but no exception was thrown" + + context); + } + } +} diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java index 1990f7ccf06..8ce172c91f7 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java @@ -332,11 +332,7 @@ public void fieldPathsMustNotHaveInvalidSegments() { List badFieldPaths = asList("foo~bar", "foo*bar", "foo/bar", "foo[1", "foo]1", "foo[1]"); for (String fieldPath : badFieldPaths) { - String reason = - "Invalid field path (" - + fieldPath - + "). Paths must not contain '~', '*', '/', '[', or ']'"; - verifyFieldPathThrows(fieldPath, reason); + verifyFieldPathThrows(fieldPath, "Use FieldPath.of() for field names containing '~*/[]'."); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java index af3360926c3..ee8c1bc51ec 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FieldPath.java @@ -84,8 +84,7 @@ public static FieldPath documentId() { static FieldPath fromDotSeparatedPath(@NonNull String path) { checkNotNull(path, "Provided field path must not be null."); checkArgument( - !RESERVED.matcher(path).find(), - "Invalid field path (" + path + "). Paths must not contain '~', '*', '/', '[', or ']'"); + !RESERVED.matcher(path).find(), "Use FieldPath.of() for field names containing '~*/[]'."); try { // By default, split() doesn't return empty leading and trailing segments. This can be enabled // by passing "-1" as the limit. diff --git a/firebase-functions/firebase-functions.gradle b/firebase-functions/firebase-functions.gradle index 64d888dd000..dce829719ba 100644 --- a/firebase-functions/firebase-functions.gradle +++ b/firebase-functions/firebase-functions.gradle @@ -29,7 +29,7 @@ android { compileSdkVersion project.targetSdkVersion defaultConfig { targetSdkVersion project.targetSdkVersion - minSdkVersion project.minSdkVersion + minSdkVersion 16 versionName version multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -50,16 +50,16 @@ android { dependencies { implementation project(':firebase-common') - 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-iid:17.0.3') { + 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-iid:19.0.0') { exclude group: 'com.google.firebase', module: 'firebase-common' } implementation ('com.google.firebase:firebase-auth-interop:17.0.0') { exclude group: 'com.google.firebase', module: 'firebase-common' } - implementation 'com.google.firebase:firebase-iid-interop:16.0.1' + implementation 'com.google.firebase:firebase-iid-interop:17.0.0' implementation 'com.squareup.okhttp3:okhttp:3.12.1' diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index 4c7da9c1258..904cf7b5e5a 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=16.3.1 -latestReleasedVersion=16.3.0 +version=18.0.1 +latestReleasedVersion=18.0.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-functions/ktx/ktx.gradle b/firebase-functions/ktx/ktx.gradle index 48a014c76ae..f2ce73b2641 100644 --- a/firebase-functions/ktx/ktx.gradle +++ b/firebase-functions/ktx/ktx.gradle @@ -27,7 +27,7 @@ firebaseLibrary { android { compileSdkVersion project.targetSdkVersion defaultConfig { - minSdkVersion project.minSdkVersion + minSdkVersion 16 multiDexEnabled true targetSdkVersion project.targetSdkVersion versionName version @@ -50,7 +50,7 @@ dependencies { implementation project(':firebase-common:ktx') implementation project(':firebase-functions') implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.android.gms:play-services-tasks:16.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' androidTestImplementation 'junit:junit:4.12' androidTestImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-functions/ktx/src/androidTest/AndroidManifest.xml b/firebase-functions/ktx/src/androidTest/AndroidManifest.xml index 53cd9caa09e..e00dc0c7ec9 100644 --- a/firebase-functions/ktx/src/androidTest/AndroidManifest.xml +++ b/firebase-functions/ktx/src/androidTest/AndroidManifest.xml @@ -1,7 +1,7 @@ - + diff --git a/firebase-functions/ktx/src/main/AndroidManifest.xml b/firebase-functions/ktx/src/main/AndroidManifest.xml index 8eda8b99bae..bcdb806609a 100644 --- a/firebase-functions/ktx/src/main/AndroidManifest.xml +++ b/firebase-functions/ktx/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + - + diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 88ea12a1ed0..dc4606715c5 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -91,12 +91,17 @@ 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:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' + 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" + androidTestImplementation 'junit:junit:4.12' } 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..5783294cfa3 --- /dev/null +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -0,0 +1,91 @@ +// 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.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; +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 = CustomInstallationIdCache.getInstance(); + } + + @After + public void cleanUp() throws Exception { + Tasks.await(cache.clearAll()); + } + + @Test + public void testReadCacheEntry_Null() { + assertNull(cache.readCacheEntryValue(firebaseApp0)); + assertNull(cache.readCacheEntryValue(firebaseApp1)); + } + + @Test + 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"); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); + assertNull(cache.readCacheEntryValue(firebaseApp1)); + + 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"); + 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 new file mode 100644 index 00000000000..5096a265714 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java @@ -0,0 +1,130 @@ +// 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.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import com.google.android.gms.common.util.Strings; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.FirebaseApp; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +class CustomInstallationIdCache { + + // 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, + // 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 SHARED_PREFS_NAME = "CustomInstallationIdCache"; + + 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 CustomInstallationIdCache singleton = null; + private final Executor ioExecuter; + private final SharedPreferences prefs; + + static synchronized CustomInstallationIdCache getInstance() { + if (singleton == null) { + singleton = new CustomInstallationIdCache(); + } + return singleton; + } + + private CustomInstallationIdCache() { + // Since different FirebaseApp in the same Android application should have the same application + // context and same dir path, so that use the context of the default FirebaseApp to create the + // shared preferences. + prefs = + FirebaseApp.getInstance() + .getApplicationContext() + .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + + ioExecuter = Executors.newFixedThreadPool(2); + } + + @Nullable + synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { + String cid = + prefs.getString(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), null); + String iid = prefs.getString(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), -1); + + if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) { + return null; + } + + return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); + } + + synchronized Task 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()); + return commitSharedPreferencesEditAsync(editor); + } + + synchronized Task clear(FirebaseApp firebaseApp) { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY)); + editor.remove(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY)); + editor.remove(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY)); + return commitSharedPreferencesEditAsync(editor); + } + + @RestrictTo(RestrictTo.Scope.TESTS) + 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); + } + + 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(); + } +} 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); + } +} diff --git a/firebase-storage/firebase-storage.gradle b/firebase-storage/firebase-storage.gradle index 4abc6369812..2fa2abb6542 100644 --- a/firebase-storage/firebase-storage.gradle +++ b/firebase-storage/firebase-storage.gradle @@ -20,6 +20,7 @@ plugins { firebaseLibrary { testLab.enabled = true publishJavadoc = true + publishSources = true } android { @@ -76,9 +77,9 @@ android { dependencies { implementation project(':firebase-common') - 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-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-storage/gradle.properties b/firebase-storage/gradle.properties index f4ae1a57594..b2337aeb5ba 100644 --- a/firebase-storage/gradle.properties +++ b/firebase-storage/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-storage/src/main/java/com/google/firebase/storage/StorageTaskManager.java b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskManager.java index f703a9d845f..8814afcfbe8 100644 --- a/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskManager.java +++ b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskManager.java @@ -32,7 +32,7 @@ /*package*/ class StorageTaskManager { private static final StorageTaskManager _instance = new StorageTaskManager(); - private final Map> inProgressTasks = new HashMap<>(); + private final Map>> inProgressTasks = new HashMap<>(); private final Object syncObject = new Object(); @@ -44,7 +44,7 @@ public List getUploadTasksUnder(@NonNull StorageReference parent) { synchronized (syncObject) { ArrayList inProgressList = new ArrayList<>(); String parentPath = parent.toString(); - for (Map.Entry> entry : inProgressTasks.entrySet()) { + for (Map.Entry>> entry : inProgressTasks.entrySet()) { if (entry.getKey().startsWith(parentPath)) { StorageTask task = entry.getValue().get(); if (task instanceof UploadTask) { @@ -60,9 +60,9 @@ public List getDownloadTasksUnder(@NonNull StorageReference pa synchronized (syncObject) { ArrayList inProgressList = new ArrayList<>(); String parentPath = parent.toString(); - for (Map.Entry> entry : inProgressTasks.entrySet()) { + for (Map.Entry>> entry : inProgressTasks.entrySet()) { if (entry.getKey().startsWith(parentPath)) { - StorageTask task = entry.getValue().get(); + StorageTask task = entry.getValue().get(); if (task instanceof FileDownloadTask) { inProgressList.add((FileDownloadTask) task); } @@ -72,19 +72,19 @@ public List getDownloadTasksUnder(@NonNull StorageReference pa } } - public void ensureRegistered(StorageTask targetTask) { + public void ensureRegistered(StorageTask targetTask) { synchronized (syncObject) { // ensure *this* is added to the in progress list inProgressTasks.put(targetTask.getStorage().toString(), new WeakReference<>(targetTask)); } } - public void unRegister(StorageTask targetTask) { + public void unRegister(StorageTask targetTask) { synchronized (syncObject) { // ensure *this* is added to the in progress list String key = targetTask.getStorage().toString(); - WeakReference weakReference = inProgressTasks.get(key); - StorageTask task = weakReference != null ? weakReference.get() : null; + WeakReference> weakReference = inProgressTasks.get(key); + StorageTask task = weakReference != null ? weakReference.get() : null; if (task == null || task == targetTask) { inProgressTasks.remove(key); } diff --git a/firebase-storage/test-app/test-app.gradle b/firebase-storage/test-app/test-app.gradle index b45d4f1b021..ab9fadb4dfd 100644 --- a/firebase-storage/test-app/test-app.gradle +++ b/firebase-storage/test-app/test-app.gradle @@ -49,10 +49,10 @@ dependencies { // We intentionally use an open ended version to pick up any SNAPSHOT // versions published to the root project' s build/ directory. - implementation 'com.google.firebase:firebase-auth:17+' - implementation 'com.google.firebase:firebase-common:17+' - 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.firebase:firebase-auth:18+' + implementation 'com.google.firebase:firebase-common:18+' + 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.material:material:1.0.0' implementation 'androidx.appcompat:appcompat:1.0.2' diff --git a/protolite-well-known-types/gradle.properties b/protolite-well-known-types/gradle.properties index c763f64467b..54be3eb478f 100644 --- a/protolite-well-known-types/gradle.properties +++ b/protolite-well-known-types/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/root-project.gradle b/root-project.gradle index 1d519f9278f..664cce0efd9 100644 --- a/root-project.gradle +++ b/root-project.gradle @@ -52,6 +52,7 @@ ext { apply plugin: com.google.firebase.gradle.plugins.publish.PublishingPlugin apply plugin: com.google.firebase.gradle.plugins.ci.ContinuousIntegrationPlugin +apply plugin: com.google.firebase.gradle.plugins.ci.SmokeTestsPlugin apply plugin: com.google.firebase.gradle.plugins.ci.metrics.MetricsPlugin firebaseContinuousIntegration { @@ -139,38 +140,6 @@ configure(subprojects) { } } -/** - * Disable "debug" build type for all subprojects. - * - * They are identical to "release" and are not used in either release or smoke tests. Disabling them - * to reduce the number of tests we run on pre/post-submit. - */ -configure(subprojects) { - afterEvaluate { Project sub -> - if (!sub.plugins.hasPlugin('com.android.library') && !sub.plugins.hasPlugin('com.android.application')) { - return - } - - // skip debug unit tests in CI - // TODO(vkryachko): provide ability for teams to control this if needed - if (System.getenv().containsKey("FIREBASE_CI")) { - sub.tasks.all {Task task -> - if (task.name == 'testDebugUnitTest') { - task.enabled = false - } - } - } - sub.android { - testBuildType "release" - - buildTypes { - // 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. - release.signingConfig = debug.signingConfig - } - } - } -} /** * Configure "Preguarding" and Desugaring for the subprojects. diff --git a/smoke-tests/build.gradle b/smoke-tests/build.gradle index 55b7f1d3aac..2c7be0fe133 100644 --- a/smoke-tests/build.gradle +++ b/smoke-tests/build.gradle @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + buildscript { repositories { google() @@ -114,4 +118,12 @@ dependencies { storageImplementation "com.google.firebase:firebase-storage" } +clean.doLast { + def paths = Files.newDirectoryStream(Paths.get("."), "build-*") + + for (Path path : paths) { + project.delete "$path/" + } +} + apply plugin: "com.google.gms.google-services" diff --git a/tools/measurement/apksize/src/firestore/firestore.gradle b/tools/measurement/apksize/src/firestore/firestore.gradle index 6b78e0f5598..f8640c43e7c 100644 --- a/tools/measurement/apksize/src/firestore/firestore.gradle +++ b/tools/measurement/apksize/src/firestore/firestore.gradle @@ -31,6 +31,6 @@ android { dependencies { firestoreImplementation project(":firebase-firestore") firestoreImplementation "com.google.android.gms:play-services-auth:16.0.1" - firestoreImplementation "com.google.android.gms:play-services-base:16.1.0" + firestoreImplementation "com.google.android.gms:play-services-base:17.0.0" firestoreImplementation 'androidx.legacy:legacy-support-v4:1.0.0' } diff --git a/transport/transport-api/gradle.properties b/transport/transport-api/gradle.properties index a9aff5b2e2e..983642acd1f 100644 --- a/transport/transport-api/gradle.properties +++ b/transport/transport-api/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=1.0.1 -latestReleasedVersion=1.0.0 +version=2.0.1 +latestReleasedVersion=2.0.0 diff --git a/transport/transport-backend-cct/gradle.properties b/transport/transport-backend-cct/gradle.properties index 4e6041d681d..7708203d64e 100644 --- a/transport/transport-backend-cct/gradle.properties +++ b/transport/transport-backend-cct/gradle.properties @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=1.0.1 -latestReleasedVersion=1.0.0 +version=2.0.1 +latestReleasedVersion=2.0.0 firebaseSkipPreguard=false diff --git a/transport/transport-runtime/gradle.properties b/transport/transport-runtime/gradle.properties index 0cbb89724a3..18a78760e82 100644 --- a/transport/transport-runtime/gradle.properties +++ b/transport/transport-runtime/gradle.properties @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=1.0.1 -latestReleasedVersion=1.0.0 +version=2.0.1 +latestReleasedVersion=2.0.0 android.enableUnitTestBinaryResources=true From dba0c0eb57aa655b60ebe88de91b121835fd9e00 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 15:16:35 -0700 Subject: [PATCH 12/74] Replace some deprecated code. --- .../segmentation/CustomInstallationIdCacheTest.java | 6 +++--- .../segmentation/FirebaseSegmentationInstrumentedTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 5783294cfa3..ced06450c3c 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; @@ -41,11 +41,11 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp0 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); firebaseApp1 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); cache = CustomInstallationIdCache.getInstance(); diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index db8fcb9b873..739df092564 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -16,7 +16,7 @@ import static org.junit.Assert.assertNull; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -39,7 +39,7 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); } From 1274b42b9226ea3f6443dd87d98fb39b2d7af16b Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Tue, 18 Jun 2019 15:21:48 -0700 Subject: [PATCH 13/74] Fix some deprecations (#533) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. --- .../segmentation/CustomInstallationIdCacheTest.java | 6 +++--- .../segmentation/FirebaseSegmentationInstrumentedTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java index 5783294cfa3..ced06450c3c 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; @@ -41,11 +41,11 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp0 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); firebaseApp1 = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); cache = CustomInstallationIdCache.getInstance(); diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index db8fcb9b873..739df092564 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -16,7 +16,7 @@ import static org.junit.Assert.assertNull; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -39,7 +39,7 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( - InstrumentationRegistry.getContext(), + ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); } From a9a43a45ac18db388618156c962fc55eeda75424 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:02:04 -0700 Subject: [PATCH 14/74] Package refactor --- .../{ => local}/CustomInstallationIdCacheTest.java | 6 ++++-- .../segmentation/{ => local}/CustomInstallationIdCache.java | 2 +- .../{ => local}/CustomInstallationIdCacheEntryValue.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) rename firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheTest.java (95%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCache.java (99%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheEntryValue.java (90%) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java similarity index 95% rename from firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java rename to firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index ced06450c3c..4a63177ba26 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; @@ -28,7 +28,9 @@ import org.junit.Test; import org.junit.runner.RunWith; -/** Instrumented tests for {@link CustomInstallationIdCache} */ +/** + * Instrumented tests for {@link com.google.firebase.segmentation.local.CustomInstallationIdCache} + */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java similarity index 99% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 5096a265714..2b93231eb39 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import android.content.Context; import android.content.SharedPreferences; diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java similarity index 90% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index c79d5f091a9..2d3b5f3c3a6 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import com.google.auto.value.AutoValue; -import com.google.firebase.segmentation.CustomInstallationIdCache.CacheStatus; +import com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus; /** * This class represents a cache entry value in {@link CustomInstallationIdCache}, which contains a From ca6dacfff2f9d9b29b0495b09927bf374892b96a Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:04:34 -0700 Subject: [PATCH 15/74] nit --- .../segmentation/local/CustomInstallationIdCacheTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 4a63177ba26..b7028f1e595 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -29,7 +29,7 @@ import org.junit.runner.RunWith; /** - * Instrumented tests for {@link com.google.firebase.segmentation.local.CustomInstallationIdCache} + * Instrumented tests for {@link CustomInstallationIdCache} */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { From e7fff8152e3e912821622703565f8741497b6b9e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 18 Jun 2019 16:05:32 -0700 Subject: [PATCH 16/74] nit --- .../segmentation/local/CustomInstallationIdCacheTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index b7028f1e595..27ab706b3a8 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -28,9 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -/** - * Instrumented tests for {@link CustomInstallationIdCache} - */ +/** Instrumented tests for {@link CustomInstallationIdCache} */ @RunWith(AndroidJUnit4.class) public class CustomInstallationIdCacheTest { From d9520039c24708e4ee90d401dca8b02e16930529 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Tue, 18 Jun 2019 16:06:15 -0700 Subject: [PATCH 17/74] package refactor (#534) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit --- .../{ => local}/CustomInstallationIdCacheTest.java | 2 +- .../segmentation/{ => local}/CustomInstallationIdCache.java | 2 +- .../{ => local}/CustomInstallationIdCacheEntryValue.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheTest.java (98%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCache.java (99%) rename firebase-segmentation/src/main/java/com/google/firebase/segmentation/{ => local}/CustomInstallationIdCacheEntryValue.java (90%) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java similarity index 98% rename from firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java rename to firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index ced06450c3c..27ab706b3a8 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java similarity index 99% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 5096a265714..2b93231eb39 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import android.content.Context; import android.content.SharedPreferences; diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java similarity index 90% rename from firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java rename to firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index c79d5f091a9..2d3b5f3c3a6 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.segmentation; +package com.google.firebase.segmentation.local; import com.google.auto.value.AutoValue; -import com.google.firebase.segmentation.CustomInstallationIdCache.CacheStatus; +import com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus; /** * This class represents a cache entry value in {@link CustomInstallationIdCache}, which contains a From b381889c8ef8e86c0f8f662538421ce1763aba5b Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 19 Jun 2019 16:47:20 -0700 Subject: [PATCH 18/74] Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) --- .../firebase-segmentation.gradle | 7 + .../FirebaseSegmentationInstrumentedTest.java | 189 +++++++++++++++++- .../local/CustomInstallationIdCacheTest.java | 31 +-- .../segmentation/FirebaseSegmentation.java | 184 ++++++++++++++++- .../SetCustomInstallationIdException.java | 68 +++++++ .../google/firebase/segmentation/Utils.java | 33 +++ .../local/CustomInstallationIdCache.java | 87 ++++---- .../CustomInstallationIdCacheEntryValue.java | 10 +- .../remote/SegmentationServiceClient.java | 56 ++++++ .../FirebaseSegmentationRegistrarTest.java | 3 - 10 files changed, 588 insertions(+), 80 deletions(-) create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index dc4606715c5..1f76b8b593a 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -83,13 +83,18 @@ android { dependencies { implementation project(':firebase-common') + implementation project(':protolite-well-known-types') implementation('com.google.firebase:firebase-iid:17.0.3') { exclude group: "com.google.firebase", module: "firebase-common" } + implementation 'io.grpc:grpc-stub:1.21.0' + implementation 'io.grpc:grpc-protobuf-lite:1.21.0' + implementation 'io.grpc:grpc-okhttp:1.21.0' implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.android.gms:play-services-tasks:16.0.1' + implementation 'com.squareup.okhttp:okhttp:2.7.5' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" @@ -104,4 +109,6 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'junit:junit:4.12' + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 739df092564..289c448fd1f 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -14,15 +14,34 @@ package com.google.firebase.segmentation; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import java.util.concurrent.ExecutionException; +import org.junit.After; import org.junit.Before; +import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Instrumented test, which will execute on an Android device. @@ -30,21 +49,185 @@ * @see Testing documentation */ @RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class FirebaseSegmentationInstrumentedTest { + private static final String CUSTOM_INSTALLATION_ID = "123"; + private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; + private FirebaseApp firebaseApp; + @Mock private FirebaseInstanceId firebaseInstanceId; + @Mock private SegmentationServiceClient backendClientReturnsOk; + @Mock private SegmentationServiceClient backendClientReturnsError; + private CustomInstallationIdCache actualCache; + @Mock private CustomInstallationIdCache cacheReturnsError; @Before public void setUp() { + MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + new FirebaseOptions.Builder() + .setApplicationId("1" + ":123456789:android:abcdef") + .build()); + actualCache = new CustomInstallationIdCache(firebaseApp); + + when(backendClientReturnsOk.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(firebaseInstanceId.getInstanceId()) + .thenReturn( + Tasks.forResult( + new InstanceIdResult() { + @NonNull + @Override + public String getId() { + return FIREBASE_INSTANCE_ID; + } + + @NonNull + @Override + public String getToken() { + return "iid_token"; + } + })); + when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(Tasks.forResult(false)); + when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); + } + + @After + public void cleanUp() throws Exception { + Tasks.await(actualCache.clear()); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendError() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } @Test - public void useAppContext() { - assertNull(FirebaseSegmentation.getInstance().setCustomInstallationId("123123").getResult()); + public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(null))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertNull(entryValue); + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(null)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId().isEmpty()).isTrue(); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_CLEAR); + } + + @Test + public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } } } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 27ab706b3a8..019a2b8ba08 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -34,7 +34,8 @@ public class CustomInstallationIdCacheTest { private FirebaseApp firebaseApp0; private FirebaseApp firebaseApp1; - private CustomInstallationIdCache cache; + private CustomInstallationIdCache cache0; + private CustomInstallationIdCache cache1; @Before public void setUp() { @@ -48,42 +49,44 @@ public void setUp() { ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); - cache = CustomInstallationIdCache.getInstance(); + cache0 = new CustomInstallationIdCache(firebaseApp0); + cache1 = new CustomInstallationIdCache(firebaseApp1); } @After public void cleanUp() throws Exception { - Tasks.await(cache.clearAll()); + Tasks.await(cache0.clear()); + Tasks.await(cache1.clear()); } @Test public void testReadCacheEntry_Null() { - assertNull(cache.readCacheEntryValue(firebaseApp0)); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + assertNull(cache0.readCacheEntryValue()); + assertNull(cache1.readCacheEntryValue()); } @Test public void testUpdateAndReadCacheEntry() throws Exception { assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)))); - CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); + "123456", + "cAAAAAAAAAA", + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)))); + CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); + assertNull(cache1.readCacheEntryValue()); assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); - entryValue = cache.readCacheEntryValue(firebaseApp0); + entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index eca517db1b3..5bed0660b8a 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -15,21 +15,45 @@ package com.google.firebase.segmentation; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.SetCustomInstallationIdException.Status; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { private final FirebaseApp firebaseApp; private final FirebaseInstanceId firebaseInstanceId; + private final CustomInstallationIdCache localCache; + private final SegmentationServiceClient backendServiceClient; FirebaseSegmentation(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); + localCache = new CustomInstallationIdCache(firebaseApp); + backendServiceClient = new SegmentationServiceClient(); + } + + @RestrictTo(RestrictTo.Scope.TESTS) + FirebaseSegmentation( + FirebaseApp firebaseApp, + FirebaseInstanceId firebaseInstanceId, + CustomInstallationIdCache localCache, + SegmentationServiceClient backendServiceClient) { + this.firebaseApp = firebaseApp; + this.firebaseInstanceId = firebaseInstanceId; + this.localCache = localCache; + this.backendServiceClient = backendServiceClient; } /** @@ -51,11 +75,165 @@ public static FirebaseSegmentation getInstance() { */ @NonNull public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { - Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + Preconditions.checkArgument(app != null, "Null is not a valid value " + "of FirebaseApp."); return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(String customInstallationId) { - return Tasks.forResult(null); + Task setCustomInstallationId(@Nullable String customInstallationId) { + if (customInstallationId == null) { + return clearCustomInstallationId(); + } + return updateCustomInstallationId(customInstallationId); + } + + /** + * Update custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and + * client side cache. + * + *

+   *     The workflow is:
+   *         check diff against cache or cache status is not SYNCED
+   *                                 |
+   *                  get Firebase instance id and token
+   *                      |                       |
+   *                      |      update cache with cache status PENDING_UPDATE
+   *                      |                       |
+   *                    send http request to backend
+   *                                 |
+   *              on success: set cache entry status to SYNCED
+   *                                 |
+   *                               return
+   * 
+ */ + private Task updateCustomInstallationId(String customInstallationId) { + CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); + if (cacheEntryValue != null + && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) + && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { + // If the given custom installation id matches up the cached + // value, there's no need to update. + return Tasks.forResult(null); + } + + Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); + Task firstUpdateCacheResultTask = + instanceIdResultTask.onSuccessTask( + instanceIdResult -> + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); + + // Start requesting backend when first cache update is done. + Task backendRequestResultTask = + firstUpdateCacheResultTask.onSuccessTask( + firstUpdateCacheResult -> { + if (firstUpdateCacheResult) { + String iid = instanceIdResultTask.getResult().getId(); + String iidToken = instanceIdResultTask.getResult().getToken(); + return backendServiceClient.updateCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + customInstallationId, + iid, + iidToken); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + + Task finalUpdateCacheResultTask = + backendRequestResultTask.onSuccessTask( + backendRequestResult -> { + switch (backendRequestResult) { + case OK: + return localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResultTask.getResult().getId(), + CustomInstallationIdCache.CacheStatus.SYNCED)); + case ALREADY_EXISTS: + throw new SetCustomInstallationIdException( + Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + }); + + return finalUpdateCacheResultTask.onSuccessTask( + finalUpdateCacheResult -> { + if (finalUpdateCacheResult) { + return Tasks.forResult(null); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + } + + /** + * Clear custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and + * client side cache. + * + *
+   *     The workflow is:
+   *                  get Firebase instance id and token
+   *                      |                      |
+   *                      |    update cache with cache status PENDING_CLEAR
+   *                      |                      |
+   *                    send http request to backend
+   *                                  |
+   *                   on success: delete cache entry
+   *                                  |
+   *                               return
+   * 
+ */ + private Task clearCustomInstallationId() { + Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); + Task firstUpdateCacheResultTask = + instanceIdResultTask.onSuccessTask( + instanceIdResult -> + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "", + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_CLEAR))); + + Task backendRequestResultTask = + firstUpdateCacheResultTask.onSuccessTask( + firstUpdateCacheResult -> { + if (firstUpdateCacheResult) { + String iid = instanceIdResultTask.getResult().getId(); + String iidToken = instanceIdResultTask.getResult().getToken(); + return backendServiceClient.clearCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + iid, + iidToken); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + + Task finalUpdateCacheResultTask = + backendRequestResultTask.onSuccessTask( + backendRequestResult -> { + if (backendRequestResult == Code.OK) { + return localCache.clear(); + } else { + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + }); + + return finalUpdateCacheResultTask.onSuccessTask( + finalUpdateCacheResult -> { + if (finalUpdateCacheResult) { + return Tasks.forResult(null); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java new file mode 100644 index 00000000000..3c957ce3294 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java @@ -0,0 +1,68 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseSegmentation}. */ +public class SetCustomInstallationIdException extends FirebaseException { + + public enum Status { + UNKOWN(0), + + /** Error in Firebase SDK. */ + CLIENT_ERROR(1), + + /** Error when calling Firebase segmentation backend. */ + BACKEND_ERROR(2), + + /** The given custom installation is already tied to another Firebase installation. */ + DUPLICATED_CUSTOM_INSTALLATION_ID(3); + + private final int value; + + Status(int value) { + this.value = value; + } + } + + @NonNull private final Status status; + + SetCustomInstallationIdException(@NonNull Status status) { + this.status = status; + } + + SetCustomInstallationIdException(@NonNull String message, @NonNull Status status) { + super(message); + this.status = status; + } + + SetCustomInstallationIdException( + @NonNull String message, @NonNull Status status, Throwable cause) { + super(message, cause); + this.status = status; + } + + /** + * Gets the status code for the operation that failed. + * + * @return the code for the SetCustomInstallationIdException + */ + @NonNull + public Status getStatus() { + return status; + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java new file mode 100644 index 00000000000..ca231a89cb5 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Util methods used for {@link FirebaseSegmentation} */ +class Utils { + + private static final Pattern APP_ID_PATTERN = + Pattern.compile("^[^:]+:([0-9]+):(android|ios|web):([0-9a-f]+)"); + + static long getProjectNumberFromAppId(String appId) { + Matcher matcher = APP_ID_PATTERN.matcher(appId); + if (matcher.matches()) { + return Long.valueOf(matcher.group(1)); + } + throw new IllegalArgumentException("Invalid app id " + appId); + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 2b93231eb39..f650c3463d8 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -17,27 +17,30 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import com.google.android.gms.common.util.Strings; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.FirebaseApp; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -class CustomInstallationIdCache { +/** + * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation + * backend API. + */ +public class CustomInstallationIdCache { // Status of each cache entry // NOTE: never change the ordinal of the enum values because the enum values are stored in cache // as their ordinal numbers. - enum CacheStatus { + public enum CacheStatus { // Cache entry is synced to Firebase backend SYNCED, - // Cache entry is waiting for Firebase backend response or pending internal retry for retryable - // errors. - PENDING, - // Cache entry is not accepted by Firebase backend. - ERROR, + // Cache entry is waiting for Firebase backend response or internal network retry (for update + // operation). + PENDING_UPDATE, + // Cache entry is waiting for Firebase backend response or internal network retry (for clear + // operation). + PENDING_CLEAR } private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; @@ -46,78 +49,58 @@ enum CacheStatus { private static final String INSTANCE_ID_KEY = "Iid"; private static final String CACHE_STATUS_KEY = "Status"; - private static CustomInstallationIdCache singleton = null; private final Executor ioExecuter; private final SharedPreferences prefs; + private final String persistenceKey; - static synchronized CustomInstallationIdCache getInstance() { - if (singleton == null) { - singleton = new CustomInstallationIdCache(); - } - return singleton; - } - - private CustomInstallationIdCache() { - // Since different FirebaseApp in the same Android application should have the same application - // context and same dir path, so that use the context of the default FirebaseApp to create the - // shared preferences. + public CustomInstallationIdCache(FirebaseApp firebaseApp) { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path prefs = - FirebaseApp.getInstance() + firebaseApp .getApplicationContext() .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - + persistenceKey = firebaseApp.getPersistenceKey(); ioExecuter = Executors.newFixedThreadPool(2); } @Nullable - synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { - String cid = - prefs.getString(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), null); - String iid = prefs.getString(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), -1); + public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { + String cid = prefs.getString(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), null); + String iid = prefs.getString(getSharedPreferencesKey(INSTANCE_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); - if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) { + if (cid == null || iid == null || status == -1) { return null; } return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } - synchronized Task insertOrUpdateCacheEntry( - FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { + public synchronized Task insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( - getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), - entryValue.getCustomInstallationId()); - editor.putString( - getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); - editor.putInt( - getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), - entryValue.getCacheStatus().ordinal()); - return commitSharedPreferencesEditAsync(editor); - } - - synchronized Task clear(FirebaseApp firebaseApp) { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY)); - editor.remove(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY)); - editor.remove(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY)); + getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); + editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); + editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); return commitSharedPreferencesEditAsync(editor); } - @RestrictTo(RestrictTo.Scope.TESTS) - synchronized Task clearAll() { + public synchronized Task clear() { SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); + editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); + editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY)); + editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); return commitSharedPreferencesEditAsync(editor); } - private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) { - return String.format("%s|%s", firebaseApp.getPersistenceKey(), key); + private String getSharedPreferencesKey(String key) { + return String.format("%s|%s", persistenceKey, key); } private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { - TaskCompletionSource result = new TaskCompletionSource(); + TaskCompletionSource result = new TaskCompletionSource<>(); ioExecuter.execute( new Runnable() { @Override diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index 2d3b5f3c3a6..05528cd40f0 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -22,14 +22,14 @@ * Firebase instance id, a custom installation id and the cache status of this entry. */ @AutoValue -abstract class CustomInstallationIdCacheEntryValue { - abstract String getCustomInstallationId(); +public abstract class CustomInstallationIdCacheEntryValue { + public abstract String getCustomInstallationId(); - abstract String getFirebaseInstanceId(); + public abstract String getFirebaseInstanceId(); - abstract CacheStatus getCacheStatus(); + public abstract CacheStatus getCacheStatus(); - static CustomInstallationIdCacheEntryValue create( + public static CustomInstallationIdCacheEntryValue create( String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { return new AutoValue_CustomInstallationIdCacheEntryValue( customInstallationId, firebaseInstanceId, cacheStatus); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java new file mode 100644 index 00000000000..59398b369d5 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation.remote; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.squareup.okhttp.OkHttpClient; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** Http client that sends request to Firebase Segmentation backend API. To be implemented */ +public class SegmentationServiceClient { + + private final OkHttpClient httpClient; + private final Executor httpRequestExecutor; + + public enum Code { + OK, + + SERVER_INTERNAL_ERROR, + + ALREADY_EXISTS, + + PERMISSION_DENIED + } + + public SegmentationServiceClient() { + httpClient = new OkHttpClient(); + httpRequestExecutor = Executors.newFixedThreadPool(4); + } + + public Task updateCustomInstallationId( + long projectNumber, + String customInstallationId, + String firebaseInstanceId, + String firebaseInstanceIdToken) { + return Tasks.forResult(Code.OK); + } + + public Task clearCustomInstallationId( + long projectNumber, String firebaseInstanceId, String firebaseInstanceIdToken) { + return Tasks.forResult(Code.OK); + } +} diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java index 1f3c441808f..56b0d120eb0 100644 --- a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java @@ -15,7 +15,6 @@ package com.google.firebase.segmentation; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.FirebaseApp; @@ -48,10 +47,8 @@ public void getFirebaseInstallationsInstance() { FirebaseSegmentation defaultSegmentation = FirebaseSegmentation.getInstance(); assertNotNull(defaultSegmentation); - assertNull(defaultSegmentation.setCustomInstallationId("12345").getResult()); FirebaseSegmentation anotherSegmentation = FirebaseSegmentation.getInstance(anotherApp); assertNotNull(anotherSegmentation); - assertNull(anotherSegmentation.setCustomInstallationId("ghdjaas").getResult()); } } From 1adcfbd864f3ed65f0898034bea59fefb0864156 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 19 Jun 2019 17:02:20 -0700 Subject: [PATCH 19/74] minor format fix --- .../segmentation/FirebaseSegmentationInstrumentedTest.java | 4 +--- .../google/firebase/segmentation/FirebaseSegmentation.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 289c448fd1f..8519e641cb7 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -69,9 +69,7 @@ public void setUp() { firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder() - .setApplicationId("1" + ":123456789:android:abcdef") - .build()); + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); actualCache = new CustomInstallationIdCache(firebaseApp); when(backendClientReturnsOk.updateCustomInstallationId( diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 5bed0660b8a..e7566cd965b 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -75,7 +75,7 @@ public static FirebaseSegmentation getInstance() { */ @NonNull public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { - Preconditions.checkArgument(app != null, "Null is not a valid value " + "of FirebaseApp."); + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); return app.get(FirebaseSegmentation.class); } From 6091f82032fc78512d64de8e9e5c2257b98cdeda Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 20 Jun 2019 13:46:10 -0700 Subject: [PATCH 20/74] Address comments #1 --- .../firebase/segmentation/FirebaseSegmentation.java | 3 ++- .../segmentation/local/CustomInstallationIdCache.java | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index e7566cd965b..34de68597c1 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -79,7 +79,8 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(@Nullable String customInstallationId) { + @NonNull + public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { if (customInstallationId == null) { return clearCustomInstallationId(); } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index f650c3463d8..a1346062440 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -101,13 +101,7 @@ private String getSharedPreferencesKey(String key) { private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { TaskCompletionSource result = new TaskCompletionSource<>(); - ioExecuter.execute( - new Runnable() { - @Override - public void run() { - result.setResult(editor.commit()); - } - }); + ioExecuter.execute(() -> result.setResult(editor.commit())); return result.getTask(); } } From e7fb811fce9cc477f65c75bf8aaa743c2b946921 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Mon, 24 Jun 2019 09:56:25 -0700 Subject: [PATCH 21/74] Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. (#545) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit * Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) * minor format fix * Address comments #1 --- .../firebase-segmentation.gradle | 7 + .../FirebaseSegmentationInstrumentedTest.java | 185 +++++++++++++++++- .../local/CustomInstallationIdCacheTest.java | 31 +-- .../segmentation/FirebaseSegmentation.java | 183 ++++++++++++++++- .../SetCustomInstallationIdException.java | 68 +++++++ .../google/firebase/segmentation/Utils.java | 33 ++++ .../local/CustomInstallationIdCache.java | 95 ++++----- .../CustomInstallationIdCacheEntryValue.java | 10 +- .../remote/SegmentationServiceClient.java | 56 ++++++ .../FirebaseSegmentationRegistrarTest.java | 3 - 10 files changed, 586 insertions(+), 85 deletions(-) create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java create mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index dc4606715c5..1f76b8b593a 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -83,13 +83,18 @@ android { dependencies { implementation project(':firebase-common') + implementation project(':protolite-well-known-types') implementation('com.google.firebase:firebase-iid:17.0.3') { exclude group: "com.google.firebase", module: "firebase-common" } + implementation 'io.grpc:grpc-stub:1.21.0' + implementation 'io.grpc:grpc-protobuf-lite:1.21.0' + implementation 'io.grpc:grpc-okhttp:1.21.0' implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.android.gms:play-services-tasks:16.0.1' + implementation 'com.squareup.okhttp:okhttp:2.7.5' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" @@ -104,4 +109,6 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'junit:junit:4.12' + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 739df092564..8519e641cb7 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -14,15 +14,34 @@ package com.google.firebase.segmentation; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import java.util.concurrent.ExecutionException; +import org.junit.After; import org.junit.Before; +import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Instrumented test, which will execute on an Android device. @@ -30,21 +49,183 @@ * @see Testing documentation */ @RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class FirebaseSegmentationInstrumentedTest { + private static final String CUSTOM_INSTALLATION_ID = "123"; + private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; + private FirebaseApp firebaseApp; + @Mock private FirebaseInstanceId firebaseInstanceId; + @Mock private SegmentationServiceClient backendClientReturnsOk; + @Mock private SegmentationServiceClient backendClientReturnsError; + private CustomInstallationIdCache actualCache; + @Mock private CustomInstallationIdCache cacheReturnsError; @Before public void setUp() { + MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + actualCache = new CustomInstallationIdCache(firebaseApp); + + when(backendClientReturnsOk.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + when(firebaseInstanceId.getInstanceId()) + .thenReturn( + Tasks.forResult( + new InstanceIdResult() { + @NonNull + @Override + public String getId() { + return FIREBASE_INSTANCE_ID; + } + + @NonNull + @Override + public String getToken() { + return "iid_token"; + } + })); + when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(Tasks.forResult(false)); + when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); + } + + @After + public void cleanUp() throws Exception { + Tasks.await(actualCache.clear()); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); + } + + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendError() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } @Test - public void useAppContext() { - assertNull(FirebaseSegmentation.getInstance().setCustomInstallationId("123123").getResult()); + public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + + // No exception, means success. + assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(null))); + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertNull(entryValue); + } + + @Test + public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { + Tasks.await( + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED))); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(null)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue.getCustomInstallationId().isEmpty()).isTrue(); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getCacheStatus()) + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_CLEAR); + } + + @Test + public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); + } } } diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 27ab706b3a8..019a2b8ba08 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -34,7 +34,8 @@ public class CustomInstallationIdCacheTest { private FirebaseApp firebaseApp0; private FirebaseApp firebaseApp1; - private CustomInstallationIdCache cache; + private CustomInstallationIdCache cache0; + private CustomInstallationIdCache cache1; @Before public void setUp() { @@ -48,42 +49,44 @@ public void setUp() { ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), "firebase_app_1"); - cache = CustomInstallationIdCache.getInstance(); + cache0 = new CustomInstallationIdCache(firebaseApp0); + cache1 = new CustomInstallationIdCache(firebaseApp1); } @After public void cleanUp() throws Exception { - Tasks.await(cache.clearAll()); + Tasks.await(cache0.clear()); + Tasks.await(cache1.clear()); } @Test public void testReadCacheEntry_Null() { - assertNull(cache.readCacheEntryValue(firebaseApp0)); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + assertNull(cache0.readCacheEntryValue()); + assertNull(cache1.readCacheEntryValue()); } @Test public void testUpdateAndReadCacheEntry() throws Exception { assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)))); - CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); + "123456", + "cAAAAAAAAAA", + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)))); + CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); - assertNull(cache.readCacheEntryValue(firebaseApp1)); + .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); + assertNull(cache1.readCacheEntryValue()); assertTrue( Tasks.await( - cache.insertOrUpdateCacheEntry( - firebaseApp0, + cache0.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); - entryValue = cache.readCacheEntryValue(firebaseApp0); + entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index eca517db1b3..34de68597c1 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -15,21 +15,45 @@ package com.google.firebase.segmentation; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.google.firebase.segmentation.SetCustomInstallationIdException.Status; +import com.google.firebase.segmentation.local.CustomInstallationIdCache; +import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; +import com.google.firebase.segmentation.remote.SegmentationServiceClient; +import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { private final FirebaseApp firebaseApp; private final FirebaseInstanceId firebaseInstanceId; + private final CustomInstallationIdCache localCache; + private final SegmentationServiceClient backendServiceClient; FirebaseSegmentation(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); + localCache = new CustomInstallationIdCache(firebaseApp); + backendServiceClient = new SegmentationServiceClient(); + } + + @RestrictTo(RestrictTo.Scope.TESTS) + FirebaseSegmentation( + FirebaseApp firebaseApp, + FirebaseInstanceId firebaseInstanceId, + CustomInstallationIdCache localCache, + SegmentationServiceClient backendServiceClient) { + this.firebaseApp = firebaseApp; + this.firebaseInstanceId = firebaseInstanceId; + this.localCache = localCache; + this.backendServiceClient = backendServiceClient; } /** @@ -55,7 +79,162 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { return app.get(FirebaseSegmentation.class); } - Task setCustomInstallationId(String customInstallationId) { - return Tasks.forResult(null); + @NonNull + public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { + if (customInstallationId == null) { + return clearCustomInstallationId(); + } + return updateCustomInstallationId(customInstallationId); + } + + /** + * Update custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and + * client side cache. + * + *
+   *     The workflow is:
+   *         check diff against cache or cache status is not SYNCED
+   *                                 |
+   *                  get Firebase instance id and token
+   *                      |                       |
+   *                      |      update cache with cache status PENDING_UPDATE
+   *                      |                       |
+   *                    send http request to backend
+   *                                 |
+   *              on success: set cache entry status to SYNCED
+   *                                 |
+   *                               return
+   * 
+ */ + private Task updateCustomInstallationId(String customInstallationId) { + CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); + if (cacheEntryValue != null + && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) + && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { + // If the given custom installation id matches up the cached + // value, there's no need to update. + return Tasks.forResult(null); + } + + Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); + Task firstUpdateCacheResultTask = + instanceIdResultTask.onSuccessTask( + instanceIdResult -> + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); + + // Start requesting backend when first cache update is done. + Task backendRequestResultTask = + firstUpdateCacheResultTask.onSuccessTask( + firstUpdateCacheResult -> { + if (firstUpdateCacheResult) { + String iid = instanceIdResultTask.getResult().getId(); + String iidToken = instanceIdResultTask.getResult().getToken(); + return backendServiceClient.updateCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + customInstallationId, + iid, + iidToken); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + + Task finalUpdateCacheResultTask = + backendRequestResultTask.onSuccessTask( + backendRequestResult -> { + switch (backendRequestResult) { + case OK: + return localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResultTask.getResult().getId(), + CustomInstallationIdCache.CacheStatus.SYNCED)); + case ALREADY_EXISTS: + throw new SetCustomInstallationIdException( + Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + }); + + return finalUpdateCacheResultTask.onSuccessTask( + finalUpdateCacheResult -> { + if (finalUpdateCacheResult) { + return Tasks.forResult(null); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + } + + /** + * Clear custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and + * client side cache. + * + *
+   *     The workflow is:
+   *                  get Firebase instance id and token
+   *                      |                      |
+   *                      |    update cache with cache status PENDING_CLEAR
+   *                      |                      |
+   *                    send http request to backend
+   *                                  |
+   *                   on success: delete cache entry
+   *                                  |
+   *                               return
+   * 
+ */ + private Task clearCustomInstallationId() { + Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); + Task firstUpdateCacheResultTask = + instanceIdResultTask.onSuccessTask( + instanceIdResult -> + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "", + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_CLEAR))); + + Task backendRequestResultTask = + firstUpdateCacheResultTask.onSuccessTask( + firstUpdateCacheResult -> { + if (firstUpdateCacheResult) { + String iid = instanceIdResultTask.getResult().getId(); + String iidToken = instanceIdResultTask.getResult().getToken(); + return backendServiceClient.clearCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + iid, + iidToken); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); + + Task finalUpdateCacheResultTask = + backendRequestResultTask.onSuccessTask( + backendRequestResult -> { + if (backendRequestResult == Code.OK) { + return localCache.clear(); + } else { + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + }); + + return finalUpdateCacheResultTask.onSuccessTask( + finalUpdateCacheResult -> { + if (finalUpdateCacheResult) { + return Tasks.forResult(null); + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + }); } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java new file mode 100644 index 00000000000..3c957ce3294 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java @@ -0,0 +1,68 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseSegmentation}. */ +public class SetCustomInstallationIdException extends FirebaseException { + + public enum Status { + UNKOWN(0), + + /** Error in Firebase SDK. */ + CLIENT_ERROR(1), + + /** Error when calling Firebase segmentation backend. */ + BACKEND_ERROR(2), + + /** The given custom installation is already tied to another Firebase installation. */ + DUPLICATED_CUSTOM_INSTALLATION_ID(3); + + private final int value; + + Status(int value) { + this.value = value; + } + } + + @NonNull private final Status status; + + SetCustomInstallationIdException(@NonNull Status status) { + this.status = status; + } + + SetCustomInstallationIdException(@NonNull String message, @NonNull Status status) { + super(message); + this.status = status; + } + + SetCustomInstallationIdException( + @NonNull String message, @NonNull Status status, Throwable cause) { + super(message, cause); + this.status = status; + } + + /** + * Gets the status code for the operation that failed. + * + * @return the code for the SetCustomInstallationIdException + */ + @NonNull + public Status getStatus() { + return status; + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java new file mode 100644 index 00000000000..ca231a89cb5 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Util methods used for {@link FirebaseSegmentation} */ +class Utils { + + private static final Pattern APP_ID_PATTERN = + Pattern.compile("^[^:]+:([0-9]+):(android|ios|web):([0-9a-f]+)"); + + static long getProjectNumberFromAppId(String appId) { + Matcher matcher = APP_ID_PATTERN.matcher(appId); + if (matcher.matches()) { + return Long.valueOf(matcher.group(1)); + } + throw new IllegalArgumentException("Invalid app id " + appId); + } +} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 2b93231eb39..a1346062440 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -17,27 +17,30 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import com.google.android.gms.common.util.Strings; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.FirebaseApp; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -class CustomInstallationIdCache { +/** + * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation + * backend API. + */ +public class CustomInstallationIdCache { // Status of each cache entry // NOTE: never change the ordinal of the enum values because the enum values are stored in cache // as their ordinal numbers. - enum CacheStatus { + public enum CacheStatus { // Cache entry is synced to Firebase backend SYNCED, - // Cache entry is waiting for Firebase backend response or pending internal retry for retryable - // errors. - PENDING, - // Cache entry is not accepted by Firebase backend. - ERROR, + // Cache entry is waiting for Firebase backend response or internal network retry (for update + // operation). + PENDING_UPDATE, + // Cache entry is waiting for Firebase backend response or internal network retry (for clear + // operation). + PENDING_CLEAR } private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; @@ -46,85 +49,59 @@ enum CacheStatus { private static final String INSTANCE_ID_KEY = "Iid"; private static final String CACHE_STATUS_KEY = "Status"; - private static CustomInstallationIdCache singleton = null; private final Executor ioExecuter; private final SharedPreferences prefs; + private final String persistenceKey; - static synchronized CustomInstallationIdCache getInstance() { - if (singleton == null) { - singleton = new CustomInstallationIdCache(); - } - return singleton; - } - - private CustomInstallationIdCache() { - // Since different FirebaseApp in the same Android application should have the same application - // context and same dir path, so that use the context of the default FirebaseApp to create the - // shared preferences. + public CustomInstallationIdCache(FirebaseApp firebaseApp) { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path prefs = - FirebaseApp.getInstance() + firebaseApp .getApplicationContext() .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - + persistenceKey = firebaseApp.getPersistenceKey(); ioExecuter = Executors.newFixedThreadPool(2); } @Nullable - synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { - String cid = - prefs.getString(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), null); - String iid = prefs.getString(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), -1); + public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { + String cid = prefs.getString(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), null); + String iid = prefs.getString(getSharedPreferencesKey(INSTANCE_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); - if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) { + if (cid == null || iid == null || status == -1) { return null; } return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } - synchronized Task insertOrUpdateCacheEntry( - FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { + public synchronized Task insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( - getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), - entryValue.getCustomInstallationId()); - editor.putString( - getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); - editor.putInt( - getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), - entryValue.getCacheStatus().ordinal()); - return commitSharedPreferencesEditAsync(editor); - } - - synchronized Task clear(FirebaseApp firebaseApp) { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY)); - editor.remove(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY)); - editor.remove(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY)); + getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); + editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); + editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); return commitSharedPreferencesEditAsync(editor); } - @RestrictTo(RestrictTo.Scope.TESTS) - synchronized Task clearAll() { + public synchronized Task clear() { SharedPreferences.Editor editor = prefs.edit(); - editor.clear(); + editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); + editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY)); + editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); return commitSharedPreferencesEditAsync(editor); } - private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) { - return String.format("%s|%s", firebaseApp.getPersistenceKey(), key); + private String getSharedPreferencesKey(String key) { + return String.format("%s|%s", persistenceKey, key); } private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { - TaskCompletionSource result = new TaskCompletionSource(); - ioExecuter.execute( - new Runnable() { - @Override - public void run() { - result.setResult(editor.commit()); - } - }); + TaskCompletionSource result = new TaskCompletionSource<>(); + ioExecuter.execute(() -> result.setResult(editor.commit())); return result.getTask(); } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index 2d3b5f3c3a6..05528cd40f0 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -22,14 +22,14 @@ * Firebase instance id, a custom installation id and the cache status of this entry. */ @AutoValue -abstract class CustomInstallationIdCacheEntryValue { - abstract String getCustomInstallationId(); +public abstract class CustomInstallationIdCacheEntryValue { + public abstract String getCustomInstallationId(); - abstract String getFirebaseInstanceId(); + public abstract String getFirebaseInstanceId(); - abstract CacheStatus getCacheStatus(); + public abstract CacheStatus getCacheStatus(); - static CustomInstallationIdCacheEntryValue create( + public static CustomInstallationIdCacheEntryValue create( String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { return new AutoValue_CustomInstallationIdCacheEntryValue( customInstallationId, firebaseInstanceId, cacheStatus); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java new file mode 100644 index 00000000000..59398b369d5 --- /dev/null +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.segmentation.remote; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.squareup.okhttp.OkHttpClient; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** Http client that sends request to Firebase Segmentation backend API. To be implemented */ +public class SegmentationServiceClient { + + private final OkHttpClient httpClient; + private final Executor httpRequestExecutor; + + public enum Code { + OK, + + SERVER_INTERNAL_ERROR, + + ALREADY_EXISTS, + + PERMISSION_DENIED + } + + public SegmentationServiceClient() { + httpClient = new OkHttpClient(); + httpRequestExecutor = Executors.newFixedThreadPool(4); + } + + public Task updateCustomInstallationId( + long projectNumber, + String customInstallationId, + String firebaseInstanceId, + String firebaseInstanceIdToken) { + return Tasks.forResult(Code.OK); + } + + public Task clearCustomInstallationId( + long projectNumber, String firebaseInstanceId, String firebaseInstanceIdToken) { + return Tasks.forResult(Code.OK); + } +} diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java index 1f3c441808f..56b0d120eb0 100644 --- a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java @@ -15,7 +15,6 @@ package com.google.firebase.segmentation; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.FirebaseApp; @@ -48,10 +47,8 @@ public void getFirebaseInstallationsInstance() { FirebaseSegmentation defaultSegmentation = FirebaseSegmentation.getInstance(); assertNotNull(defaultSegmentation); - assertNull(defaultSegmentation.setCustomInstallationId("12345").getResult()); FirebaseSegmentation anotherSegmentation = FirebaseSegmentation.getInstance(anotherApp); assertNotNull(anotherSegmentation); - assertNull(anotherSegmentation.setCustomInstallationId("ghdjaas").getResult()); } } From 6a0f50241fe81025e2c6c48e803cdf78abb6e5f6 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 26 Jun 2019 13:21:20 -0700 Subject: [PATCH 22/74] Http client in Firebase Segmentation SDK to call backend service. --- .../firebase-segmentation.gradle | 45 +----- .../segmentation/FirebaseSegmentation.java | 17 +- .../remote/SegmentationServiceClient.java | 153 +++++++++++++++++- 3 files changed, 165 insertions(+), 50 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 1f76b8b593a..70b0f82fa14 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -14,45 +14,12 @@ plugins { id 'firebase-library' - id 'com.google.protobuf' } firebaseLibrary { testLab.enabled = true } -protobuf { - // Configure the protoc executable - protoc { - // Download from repositories - artifact = 'com.google.protobuf:protoc:3.4.0' - } - plugins { - grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.12.0' - } - javalite { - // The codegen for lite comes as a separate artifact - artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - // In most cases you don't need the full Java output - // if you use the lite output. - remove java - } - task.plugins { - grpc { - option 'lite' - } - javalite {} - } - } - } -} - android { compileSdkVersion project.targetSdkVersion @@ -65,8 +32,11 @@ android { } sourceSets { main { - proto { - srcDir 'src/main/proto' + java { + } + } + test { + java { } } } @@ -83,14 +53,11 @@ android { dependencies { implementation project(':firebase-common') - implementation project(':protolite-well-known-types') implementation('com.google.firebase:firebase-iid:17.0.3') { exclude group: "com.google.firebase", module: "firebase-common" } - implementation 'io.grpc:grpc-stub:1.21.0' - implementation 'io.grpc:grpc-protobuf-lite:1.21.0' - implementation 'io.grpc:grpc-okhttp:1.21.0' + implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.multidex:multidex:2.0.0' implementation 'com.google.android.gms:play-services-tasks:16.0.1' diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 34de68597c1..6a7550cb5a4 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -135,6 +135,7 @@ private Task updateCustomInstallationId(String customInstallationId) { String iidToken = instanceIdResultTask.getResult().getToken(); return backendServiceClient.updateCustomInstallationId( Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), customInstallationId, iid, iidToken); @@ -154,7 +155,9 @@ private Task updateCustomInstallationId(String customInstallationId) { customInstallationId, instanceIdResultTask.getResult().getId(), CustomInstallationIdCache.CacheStatus.SYNCED)); - case ALREADY_EXISTS: + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + case CONFLICT: throw new SetCustomInstallationIdException( Status.DUPLICATED_CUSTOM_INSTALLATION_ID); default: @@ -209,6 +212,7 @@ private Task clearCustomInstallationId() { String iidToken = instanceIdResultTask.getResult().getToken(); return backendServiceClient.clearCustomInstallationId( Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), iid, iidToken); } else { @@ -220,10 +224,13 @@ private Task clearCustomInstallationId() { Task finalUpdateCacheResultTask = backendRequestResultTask.onSuccessTask( backendRequestResult -> { - if (backendRequestResult == Code.OK) { - return localCache.clear(); - } else { - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + switch (backendRequestResult) { + case OK: + return localCache.clear(); + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } }); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 59398b369d5..5292bc74180 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -15,25 +15,47 @@ package com.google.firebase.segmentation.remote; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; +import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import java.io.IOException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.json.JSONException; +import org.json.JSONObject; /** Http client that sends request to Firebase Segmentation backend API. To be implemented */ public class SegmentationServiceClient { + private static final String FIREBASE_SEGMENTATION_API_DOMAIN = + "firebasesegmentation.googleapis.com"; + private static final String UPDATE_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/customSegmentationData"; + private static final String CLEAR_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/customSegmentationData:clear"; + private static final String FIREBASE_SEGMENTATION_API_VERSION = "alpha1"; + + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private final OkHttpClient httpClient; private final Executor httpRequestExecutor; public enum Code { OK, - SERVER_INTERNAL_ERROR, + HTTP_CLIENT_ERROR, + + CONFLICT, - ALREADY_EXISTS, + NETWORK_ERROR, - PERMISSION_DENIED + SERVER_ERROR, + + UNAUTHORIZED, } public SegmentationServiceClient() { @@ -43,14 +65,133 @@ public SegmentationServiceClient() { public Task updateCustomInstallationId( long projectNumber, + String apiKey, String customInstallationId, String firebaseInstanceId, String firebaseInstanceIdToken) { - return Tasks.forResult(Code.OK); + String resourceName = + String.format(UPDATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); + + RequestBody requestBody; + try { + requestBody = + RequestBody.create( + JSON, + buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) + .toString()); + } catch (JSONException e) { + return Tasks.forException(e); + } + + Request request = + new Request.Builder() + .url( + String.format( + "https://%s/%s/%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName)) + .header("X-Goog-Api-Key", apiKey) + .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) + .header("Content-Type", "application/json") + .patch(requestBody) + .build(); + + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + httpRequestExecutor.execute( + () -> { + try { + Response response = httpClient.newCall(request).execute(); + switch (response.code()) { + case 200: + taskCompletionSource.setResult(Code.OK); + break; + case 401: + taskCompletionSource.setResult(Code.UNAUTHORIZED); + break; + case 409: + taskCompletionSource.setResult(Code.CONFLICT); + break; + default: + taskCompletionSource.setResult(Code.SERVER_ERROR); + break; + } + } catch (IOException e) { + taskCompletionSource.setResult(Code.NETWORK_ERROR); + } + }); + return taskCompletionSource.getTask(); + } + + private static JSONObject buildUpdateCustomSegmentationDataRequestBody( + String resourceName, String customInstallationId) throws JSONException { + JSONObject rlt = new JSONObject(); + rlt.put( + "update_mask", + "custom_segmentation_data.name,custom_segmentation_data.custom_installation_id"); + JSONObject customSegmentationData = new JSONObject(); + customSegmentationData.put("name", resourceName); + customSegmentationData.put("custom_installation_id", customInstallationId); + rlt.put("custom_segmentation_data", customSegmentationData); + return rlt; } public Task clearCustomInstallationId( - long projectNumber, String firebaseInstanceId, String firebaseInstanceIdToken) { - return Tasks.forResult(Code.OK); + long projectNumber, + String apiKey, + String firebaseInstanceId, + String firebaseInstanceIdToken) { + String resourceName = + String.format(CLEAR_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); + + RequestBody requestBody; + try { + requestBody = + RequestBody.create( + JSON, buildClearCustomSegmentationDataRequestBody(resourceName).toString()); + } catch (JSONException e) { + return Tasks.forException(e); + } + + Request request = + new Request.Builder() + .url( + String.format( + "https://%s/%s/%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName)) + .header("X-Goog-Api-Key", apiKey) + .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) + .header("Content-Type", "application/json") + .post(requestBody) + .build(); + + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + httpRequestExecutor.execute( + () -> { + try { + Response response = httpClient.newCall(request).execute(); + switch (response.code()) { + case 200: + taskCompletionSource.setResult(Code.OK); + break; + case 401: + taskCompletionSource.setResult(Code.UNAUTHORIZED); + break; + default: + taskCompletionSource.setResult(Code.SERVER_ERROR); + break; + } + } catch (IOException e) { + taskCompletionSource.setResult(Code.NETWORK_ERROR); + } + }); + return taskCompletionSource.getTask(); + } + + private static JSONObject buildClearCustomSegmentationDataRequestBody(String resourceName) + throws JSONException { + return new JSONObject().put("name", resourceName); } } From 7f4097836d1787bc1f267c2170dc318738423479 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 26 Jun 2019 13:26:07 -0700 Subject: [PATCH 23/74] Revert unintentional change --- subprojects.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/subprojects.cfg b/subprojects.cfg index e27229cee5b..6a09b6d4a45 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -15,7 +15,6 @@ firebase-inappmessaging-display firebase-storage firebase-storage:test-app firebase-segmentation - protolite-well-known-types tools:errorprone From b2fc302762cb4562646587466a08eb7e244c5928 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 26 Jun 2019 13:39:30 -0700 Subject: [PATCH 24/74] Fix connected device test --- .../FirebaseSegmentationInstrumentedTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 8519e641cb7..3bac1321da6 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -69,19 +69,24 @@ public void setUp() { firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + new FirebaseOptions.Builder() + .setApplicationId("1" + ":123456789:android:abcdef") + .setApiKey("api_key") + .build()); actualCache = new CustomInstallationIdCache(firebaseApp); when(backendClientReturnsOk.updateCustomInstallationId( - anyLong(), anyString(), anyString(), anyString())) + anyLong(), anyString(), anyString(), anyString(), anyString())) .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); - when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) + when(backendClientReturnsOk.clearCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); + when(backendClientReturnsError.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); - when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); when(firebaseInstanceId.getInstanceId()) .thenReturn( Tasks.forResult( From 1f2ab34e45e6414d8663a70099650d0feac2b8f2 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 26 Jun 2019 13:39:30 -0700 Subject: [PATCH 25/74] Fix connected device test --- .../FirebaseSegmentationInstrumentedTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 8519e641cb7..a2ac5f52b90 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -69,19 +69,24 @@ public void setUp() { firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + new FirebaseOptions.Builder() + .setApplicationId("1:123456789:android:abcdef") + .setApiKey("api_key") + .build()); actualCache = new CustomInstallationIdCache(firebaseApp); when(backendClientReturnsOk.updateCustomInstallationId( - anyLong(), anyString(), anyString(), anyString())) + anyLong(), anyString(), anyString(), anyString(), anyString())) .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); - when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) + when(backendClientReturnsOk.clearCustomInstallationId( + anyLong(), anyString(), anyString(), anyString())) .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); + when(backendClientReturnsError.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); - when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); when(firebaseInstanceId.getInstanceId()) .thenReturn( Tasks.forResult( From 9880624380da20818bcc43a9902cd6bf9b2bba73 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 8 Jul 2019 12:46:15 -0700 Subject: [PATCH 26/74] 1. Add a few annotations to make java code Kotlin friendly 2. Some fixes for the http request format --- .../firebase-segmentation.gradle | 2 +- .../segmentation/FirebaseSegmentation.java | 1 + .../FirebaseSegmentationRegistrar.java | 2 ++ .../local/CustomInstallationIdCache.java | 7 ++-- .../CustomInstallationIdCacheEntryValue.java | 9 ++++- .../remote/SegmentationServiceClient.java | 36 +++++++++---------- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 70b0f82fa14..fa48a4ac58e 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -70,7 +70,7 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" - androidTestImplementation "androidx.annotation:annotation:1.1.0" + androidTestImplementation "androidx.annotation:annotation:1.0.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 6a7550cb5a4..26af8c6a817 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; + import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java index 7d1d5fcfaab..ca7f688d60a 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java @@ -14,6 +14,7 @@ package com.google.firebase.segmentation; +import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; @@ -25,6 +26,7 @@ public class FirebaseSegmentationRegistrar implements ComponentRegistrar { @Override + @NonNull public List> getComponents() { return Arrays.asList( Component.builder(FirebaseSegmentation.class) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index a1346062440..05f652d4723 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -16,6 +16,7 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -53,7 +54,7 @@ public enum CacheStatus { private final SharedPreferences prefs; private final String persistenceKey; - public CustomInstallationIdCache(FirebaseApp firebaseApp) { + public CustomInstallationIdCache(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path prefs = @@ -77,8 +78,9 @@ public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } + @NonNull public synchronized Task insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue entryValue) { + @NonNull CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); @@ -87,6 +89,7 @@ public synchronized Task insertOrUpdateCacheEntry( return commitSharedPreferencesEditAsync(editor); } + @NonNull public synchronized Task clear() { SharedPreferences.Editor editor = prefs.edit(); editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index 05528cd40f0..5e2c1944278 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -14,6 +14,7 @@ package com.google.firebase.segmentation.local; +import androidx.annotation.NonNull; import com.google.auto.value.AutoValue; import com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus; @@ -23,14 +24,20 @@ */ @AutoValue public abstract class CustomInstallationIdCacheEntryValue { + @NonNull public abstract String getCustomInstallationId(); + @NonNull public abstract String getFirebaseInstanceId(); + @NonNull public abstract CacheStatus getCacheStatus(); + @NonNull public static CustomInstallationIdCacheEntryValue create( - String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { + @NonNull String customInstallationId, + @NonNull String firebaseInstanceId, + @NonNull CacheStatus cacheStatus) { return new AutoValue_CustomInstallationIdCacheEntryValue( customInstallationId, firebaseInstanceId, cacheStatus); } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 5292bc74180..71dfb19c6c9 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -14,6 +14,7 @@ package com.google.firebase.segmentation.remote; +import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; @@ -63,12 +64,13 @@ public SegmentationServiceClient() { httpRequestExecutor = Executors.newFixedThreadPool(4); } + @NonNull public Task updateCustomInstallationId( long projectNumber, - String apiKey, - String customInstallationId, - String firebaseInstanceId, - String firebaseInstanceIdToken) { + @NonNull String apiKey, + @NonNull String customInstallationId, + @NonNull String firebaseInstanceId, + @NonNull String firebaseInstanceIdToken) { String resourceName = String.format(UPDATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); @@ -87,11 +89,11 @@ public Task updateCustomInstallationId( new Request.Builder() .url( String.format( - "https://%s/%s/%s", + "https://%s/%s/%s?key=%s", FIREBASE_SEGMENTATION_API_DOMAIN, FIREBASE_SEGMENTATION_API_VERSION, - resourceName)) - .header("X-Goog-Api-Key", apiKey) + resourceName, + apiKey)) .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) .header("Content-Type", "application/json") .patch(requestBody) @@ -125,22 +127,18 @@ public Task updateCustomInstallationId( private static JSONObject buildUpdateCustomSegmentationDataRequestBody( String resourceName, String customInstallationId) throws JSONException { - JSONObject rlt = new JSONObject(); - rlt.put( - "update_mask", - "custom_segmentation_data.name,custom_segmentation_data.custom_installation_id"); JSONObject customSegmentationData = new JSONObject(); customSegmentationData.put("name", resourceName); customSegmentationData.put("custom_installation_id", customInstallationId); - rlt.put("custom_segmentation_data", customSegmentationData); - return rlt; + return customSegmentationData; } + @NonNull public Task clearCustomInstallationId( long projectNumber, - String apiKey, - String firebaseInstanceId, - String firebaseInstanceIdToken) { + @NonNull String apiKey, + @NonNull String firebaseInstanceId, + @NonNull String firebaseInstanceIdToken) { String resourceName = String.format(CLEAR_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); @@ -157,11 +155,11 @@ public Task clearCustomInstallationId( new Request.Builder() .url( String.format( - "https://%s/%s/%s", + "https://%s/%s/%s?key=%s", FIREBASE_SEGMENTATION_API_DOMAIN, FIREBASE_SEGMENTATION_API_VERSION, - resourceName)) - .header("X-Goog-Api-Key", apiKey) + resourceName, + apiKey)) .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) .header("Content-Type", "application/json") .post(requestBody) From 8ffb5bb6e35e4b753c0eb6376a66f964ad37db4e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 8 Jul 2019 13:26:45 -0700 Subject: [PATCH 27/74] Fix java format --- .../com/google/firebase/segmentation/FirebaseSegmentation.java | 1 - 1 file changed, 1 deletion(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 26af8c6a817..6a7550cb5a4 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -17,7 +17,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; - import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; From daf4698905e6fb54bd4f9fd190c2b2dfc8e51c83 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 8 Jul 2019 13:46:38 -0700 Subject: [PATCH 28/74] Fix API version --- .../firebase/segmentation/remote/SegmentationServiceClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 71dfb19c6c9..e258b84ad06 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -38,7 +38,7 @@ public class SegmentationServiceClient { "projects/%s/installations/%s/customSegmentationData"; private static final String CLEAR_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s/customSegmentationData:clear"; - private static final String FIREBASE_SEGMENTATION_API_VERSION = "alpha1"; + private static final String FIREBASE_SEGMENTATION_API_VERSION = "v1alpha"; private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); From 7985e146526d6b648d322ba81b3da5a752c22df5 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 9 Jul 2019 10:55:56 -0700 Subject: [PATCH 29/74] Change the segmentation API implementation to synchronous and put the entire synchronous code block in async task. --- .../firebase-segmentation.gradle | 14 +- .../FirebaseSegmentationInstrumentedTest.java | 34 ++- .../local/CustomInstallationIdCacheTest.java | 21 +- .../segmentation/FirebaseSegmentation.java | 229 ++++++++++-------- .../local/CustomInstallationIdCache.java | 20 +- .../remote/SegmentationServiceClient.java | 90 +++---- 6 files changed, 186 insertions(+), 222 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index fa48a4ac58e..d16e46b402b 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -30,16 +30,6 @@ android { versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - sourceSets { - main { - java { - } - } - test { - java { - } - } - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -59,8 +49,8 @@ dependencies { } implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.multidex:multidex:2.0.0' - implementation 'com.google.android.gms:play-services-tasks:16.0.1' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' implementation 'com.squareup.okhttp:okhttp:2.7.5' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" 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 a2ac5f52b90..19498782a78 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 @@ -77,16 +77,16 @@ public void setUp() { when(backendClientReturnsOk.updateCustomInstallationId( anyLong(), anyString(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + .thenReturn(SegmentationServiceClient.Code.OK); when(backendClientReturnsOk.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + .thenReturn(SegmentationServiceClient.Code.OK); when(backendClientReturnsError.updateCustomInstallationId( anyLong(), anyString(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); + .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); when(backendClientReturnsError.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_ERROR)); + .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); when(firebaseInstanceId.getInstanceId()) .thenReturn( Tasks.forResult( @@ -103,13 +103,13 @@ public String getToken() { return "iid_token"; } })); - when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(Tasks.forResult(false)); + when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false); when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); } @After public void cleanUp() throws Exception { - Tasks.await(actualCache.clear()); + actualCache.clear(); } @Test @@ -170,12 +170,11 @@ public void testUpdateCustomInstallationId_CacheError_BackendOk() throws Interru @Test public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { - Tasks.await( - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED))); + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); @@ -188,12 +187,11 @@ public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { @Test public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { - Tasks.await( - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED))); + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 019a2b8ba08..9e22e522d2b 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -20,7 +20,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import org.junit.After; @@ -55,8 +54,8 @@ public void setUp() { @After public void cleanUp() throws Exception { - Tasks.await(cache0.clear()); - Tasks.await(cache1.clear()); + cache0.clear(); + cache1.clear(); } @Test @@ -68,12 +67,9 @@ public void testReadCacheEntry_Null() { @Test public void testUpdateAndReadCacheEntry() throws Exception { assertTrue( - Tasks.await( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", - "cAAAAAAAAAA", - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)))); + cache0.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); @@ -82,10 +78,9 @@ public void testUpdateAndReadCacheEntry() throws Exception { assertNull(cache1.readCacheEntryValue()); assertTrue( - Tasks.await( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); + cache0.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED))); entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 6a7550cb5a4..7a6d879d669 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -19,7 +19,7 @@ import androidx.annotation.RestrictTo; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; +import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.InstanceIdResult; @@ -28,6 +28,8 @@ import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; import com.google.firebase.segmentation.remote.SegmentationServiceClient; import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { @@ -36,12 +38,14 @@ public class FirebaseSegmentation { private final FirebaseInstanceId firebaseInstanceId; private final CustomInstallationIdCache localCache; private final SegmentationServiceClient backendServiceClient; + private final Executor executor; FirebaseSegmentation(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); - localCache = new CustomInstallationIdCache(firebaseApp); - backendServiceClient = new SegmentationServiceClient(); + this.localCache = new CustomInstallationIdCache(firebaseApp); + this.backendServiceClient = new SegmentationServiceClient(); + this.executor = Executors.newFixedThreadPool(4); } @RestrictTo(RestrictTo.Scope.TESTS) @@ -54,6 +58,7 @@ public class FirebaseSegmentation { this.firebaseInstanceId = firebaseInstanceId; this.localCache = localCache; this.backendServiceClient = backendServiceClient; + this.executor = Executors.newFixedThreadPool(4); } /** @@ -107,73 +112,79 @@ public synchronized Task setCustomInstallationId(@Nullable String customIn * */ private Task updateCustomInstallationId(String customInstallationId) { - CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); - if (cacheEntryValue != null - && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) - && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { - // If the given custom installation id matches up the cached - // value, there's no need to update. - return Tasks.forResult(null); - } + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + + executor.execute( + () -> { + CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); + if (cacheEntryValue != null + && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) + && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { + // If the given custom installation id matches up the cached + // value, there's no need to update. + taskCompletionSource.setResult(null); + return; + } - Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); - Task firstUpdateCacheResultTask = - instanceIdResultTask.onSuccessTask( - instanceIdResult -> - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); - - // Start requesting backend when first cache update is done. - Task backendRequestResultTask = - firstUpdateCacheResultTask.onSuccessTask( - firstUpdateCacheResult -> { - if (firstUpdateCacheResult) { - String iid = instanceIdResultTask.getResult().getId(); - String iidToken = instanceIdResultTask.getResult().getToken(); - return backendServiceClient.updateCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - customInstallationId, - iid, - iidToken); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); - - Task finalUpdateCacheResultTask = - backendRequestResultTask.onSuccessTask( - backendRequestResult -> { - switch (backendRequestResult) { - case OK: - return localCache.insertOrUpdateCacheEntry( + InstanceIdResult instanceIdResult = firebaseInstanceId.getInstanceId().getResult(); + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); + + if (!firstUpdateCacheResult) { + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR)); + return; + } + + // Start requesting backend when first cache updae is done. + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.updateCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + customInstallationId, + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( customInstallationId, - instanceIdResultTask.getResult().getId(), + instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.SYNCED)); - case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); - case CONFLICT: - throw new SetCustomInstallationIdException( - Status.DUPLICATED_CUSTOM_INSTALLATION_ID); - default: - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - }); - - return finalUpdateCacheResultTask.onSuccessTask( - finalUpdateCacheResult -> { + break; + case HTTP_CLIENT_ERROR: + taskCompletionSource.setException( + new SetCustomInstallationIdException(Status.CLIENT_ERROR)); + return; + case CONFLICT: + taskCompletionSource.setException( + new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID)); + return; + default: + taskCompletionSource.setException( + new SetCustomInstallationIdException(Status.BACKEND_ERROR)); + return; + } + if (finalUpdateCacheResult) { - return Tasks.forResult(null); + taskCompletionSource.setResult(null); } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR)); } }); + return taskCompletionSource.getTask(); } /** @@ -194,54 +205,58 @@ private Task updateCustomInstallationId(String customInstallationId) { * */ private Task clearCustomInstallationId() { - Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); - Task firstUpdateCacheResultTask = - instanceIdResultTask.onSuccessTask( - instanceIdResult -> - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "", - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_CLEAR))); - - Task backendRequestResultTask = - firstUpdateCacheResultTask.onSuccessTask( - firstUpdateCacheResult -> { - if (firstUpdateCacheResult) { - String iid = instanceIdResultTask.getResult().getId(); - String iidToken = instanceIdResultTask.getResult().getToken(); - return backendServiceClient.clearCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - iid, - iidToken); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); - - Task finalUpdateCacheResultTask = - backendRequestResultTask.onSuccessTask( - backendRequestResult -> { - switch (backendRequestResult) { - case OK: - return localCache.clear(); - case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); - default: - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - }); - - return finalUpdateCacheResultTask.onSuccessTask( - finalUpdateCacheResult -> { + + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + + executor.execute( + () -> { + InstanceIdResult instanceIdResult = firebaseInstanceId.getInstanceId().getResult(); + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "", + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); + + if (!firstUpdateCacheResult) { + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR)); + return; + } + + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.clearCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = localCache.clear(); + break; + case HTTP_CLIENT_ERROR: + taskCompletionSource.setException( + new SetCustomInstallationIdException(Status.CLIENT_ERROR)); + return; + default: + taskCompletionSource.setException( + new SetCustomInstallationIdException(Status.BACKEND_ERROR)); + return; + } + if (finalUpdateCacheResult) { - return Tasks.forResult(null); + taskCompletionSource.setResult(null); } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR)); } }); + return taskCompletionSource.getTask(); } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index 05f652d4723..307d5d49923 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -18,11 +18,7 @@ import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -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; /** * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation @@ -50,7 +46,6 @@ public enum CacheStatus { private static final String INSTANCE_ID_KEY = "Iid"; private static final String CACHE_STATUS_KEY = "Status"; - private final Executor ioExecuter; private final SharedPreferences prefs; private final String persistenceKey; @@ -62,7 +57,6 @@ public CustomInstallationIdCache(@NonNull FirebaseApp firebaseApp) { .getApplicationContext() .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); persistenceKey = firebaseApp.getPersistenceKey(); - ioExecuter = Executors.newFixedThreadPool(2); } @Nullable @@ -79,32 +73,26 @@ public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { } @NonNull - public synchronized Task insertOrUpdateCacheEntry( + public synchronized boolean insertOrUpdateCacheEntry( @NonNull CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); - return commitSharedPreferencesEditAsync(editor); + return editor.commit(); } @NonNull - public synchronized Task clear() { + public synchronized boolean clear() { SharedPreferences.Editor editor = prefs.edit(); editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY)); editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); - return commitSharedPreferencesEditAsync(editor); + return editor.commit(); } private String getSharedPreferencesKey(String key) { return String.format("%s|%s", persistenceKey, key); } - - private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { - TaskCompletionSource result = new TaskCompletionSource<>(); - ioExecuter.execute(() -> result.setResult(editor.commit())); - return result.getTask(); - } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index e258b84ad06..a88ecd7b662 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -15,17 +15,12 @@ package com.google.firebase.segmentation.remote; import androidx.annotation.NonNull; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import java.io.IOException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import org.json.JSONException; import org.json.JSONObject; @@ -43,7 +38,6 @@ public class SegmentationServiceClient { private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private final OkHttpClient httpClient; - private final Executor httpRequestExecutor; public enum Code { OK, @@ -61,11 +55,10 @@ public enum Code { public SegmentationServiceClient() { httpClient = new OkHttpClient(); - httpRequestExecutor = Executors.newFixedThreadPool(4); } @NonNull - public Task updateCustomInstallationId( + public Code updateCustomInstallationId( long projectNumber, @NonNull String apiKey, @NonNull String customInstallationId, @@ -82,7 +75,8 @@ public Task updateCustomInstallationId( buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) .toString()); } catch (JSONException e) { - return Tasks.forException(e); + // This should never happen + throw new IllegalStateException(e); } Request request = @@ -99,30 +93,21 @@ public Task updateCustomInstallationId( .patch(requestBody) .build(); - TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - httpRequestExecutor.execute( - () -> { - try { - Response response = httpClient.newCall(request).execute(); - switch (response.code()) { - case 200: - taskCompletionSource.setResult(Code.OK); - break; - case 401: - taskCompletionSource.setResult(Code.UNAUTHORIZED); - break; - case 409: - taskCompletionSource.setResult(Code.CONFLICT); - break; - default: - taskCompletionSource.setResult(Code.SERVER_ERROR); - break; - } - } catch (IOException e) { - taskCompletionSource.setResult(Code.NETWORK_ERROR); - } - }); - return taskCompletionSource.getTask(); + try { + Response response = httpClient.newCall(request).execute(); + switch (response.code()) { + case 200: + return Code.OK; + case 401: + return Code.UNAUTHORIZED; + case 409: + return Code.CONFLICT; + default: + return Code.SERVER_ERROR; + } + } catch (IOException e) { + return Code.NETWORK_ERROR; + } } private static JSONObject buildUpdateCustomSegmentationDataRequestBody( @@ -134,7 +119,7 @@ private static JSONObject buildUpdateCustomSegmentationDataRequestBody( } @NonNull - public Task clearCustomInstallationId( + public Code clearCustomInstallationId( long projectNumber, @NonNull String apiKey, @NonNull String firebaseInstanceId, @@ -148,7 +133,8 @@ public Task clearCustomInstallationId( RequestBody.create( JSON, buildClearCustomSegmentationDataRequestBody(resourceName).toString()); } catch (JSONException e) { - return Tasks.forException(e); + // This should never happen + throw new IllegalStateException(e); } Request request = @@ -165,27 +151,19 @@ public Task clearCustomInstallationId( .post(requestBody) .build(); - TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - httpRequestExecutor.execute( - () -> { - try { - Response response = httpClient.newCall(request).execute(); - switch (response.code()) { - case 200: - taskCompletionSource.setResult(Code.OK); - break; - case 401: - taskCompletionSource.setResult(Code.UNAUTHORIZED); - break; - default: - taskCompletionSource.setResult(Code.SERVER_ERROR); - break; - } - } catch (IOException e) { - taskCompletionSource.setResult(Code.NETWORK_ERROR); - } - }); - return taskCompletionSource.getTask(); + try { + Response response = httpClient.newCall(request).execute(); + switch (response.code()) { + case 200: + return Code.OK; + case 401: + return Code.UNAUTHORIZED; + default: + return Code.SERVER_ERROR; + } + } catch (IOException e) { + return Code.NETWORK_ERROR; + } } private static JSONObject buildClearCustomSegmentationDataRequestBody(String resourceName) From 9f36d3523761ddfe9bfb15af527f0be223134048 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 9 Jul 2019 11:08:01 -0700 Subject: [PATCH 30/74] Fix a async getResult race issue. --- .../segmentation/FirebaseSegmentation.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 7a6d879d669..12b5ff52b88 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -20,6 +20,7 @@ import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.InstanceIdResult; @@ -126,7 +127,16 @@ private Task updateCustomInstallationId(String customInstallationId) { return; } - InstanceIdResult instanceIdResult = firebaseInstanceId.getInstanceId().getResult(); + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (Exception e) { + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR)); + return; + } + boolean firstUpdateCacheResult = localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( @@ -210,7 +220,16 @@ private Task clearCustomInstallationId() { executor.execute( () -> { - InstanceIdResult instanceIdResult = firebaseInstanceId.getInstanceId().getResult(); + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (Exception e) { + taskCompletionSource.setException( + new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR)); + return; + } + boolean firstUpdateCacheResult = localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( From 047c0af81eff97623b823400b0d9293d1a1d449f Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 10 Jul 2019 09:56:56 -0700 Subject: [PATCH 31/74] OkHttpClient -> HttpsUrlConnection --- .../firebase-segmentation.gradle | 1 - .../remote/SegmentationServiceClient.java | 122 ++++++++---------- 2 files changed, 55 insertions(+), 68 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index d16e46b402b..c11085bd161 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -51,7 +51,6 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.0' - implementation 'com.squareup.okhttp:okhttp:2.7.5' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index a88ecd7b662..042afef6fc2 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -15,12 +15,10 @@ package com.google.firebase.segmentation.remote; import androidx.annotation.NonNull; -import com.squareup.okhttp.MediaType; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.Response; import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import javax.net.ssl.HttpsURLConnection; import org.json.JSONException; import org.json.JSONObject; @@ -35,10 +33,6 @@ public class SegmentationServiceClient { "projects/%s/installations/%s/customSegmentationData:clear"; private static final String FIREBASE_SEGMENTATION_API_VERSION = "v1alpha"; - private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - - private final OkHttpClient httpClient; - public enum Code { OK, @@ -53,10 +47,6 @@ public enum Code { UNAUTHORIZED, } - public SegmentationServiceClient() { - httpClient = new OkHttpClient(); - } - @NonNull public Code updateCustomInstallationId( long projectNumber, @@ -66,36 +56,35 @@ public Code updateCustomInstallationId( @NonNull String firebaseInstanceIdToken) { String resourceName = String.format(UPDATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); - - RequestBody requestBody; try { - requestBody = - RequestBody.create( - JSON, - buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) - .toString()); - } catch (JSONException e) { - // This should never happen - throw new IllegalStateException(e); - } - - Request request = - new Request.Builder() - .url( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_SEGMENTATION_API_DOMAIN, - FIREBASE_SEGMENTATION_API_VERSION, - resourceName, - apiKey)) - .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) - .header("Content-Type", "application/json") - .patch(requestBody) - .build(); + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("PATCH"); + httpsURLConnection.addRequestProperty( + "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); + httpsURLConnection.addRequestProperty("Content-Type", "application/json"); + OutputStream os = httpsURLConnection.getOutputStream(); + try { + os.write( + buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) + .toString() + .getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } + httpsURLConnection.connect(); - try { - Response response = httpClient.newCall(request).execute(); - switch (response.code()) { + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { case 200: return Code.OK; case 401: @@ -126,34 +115,33 @@ public Code clearCustomInstallationId( @NonNull String firebaseInstanceIdToken) { String resourceName = String.format(CLEAR_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); - - RequestBody requestBody; try { - requestBody = - RequestBody.create( - JSON, buildClearCustomSegmentationDataRequestBody(resourceName).toString()); - } catch (JSONException e) { - // This should never happen - throw new IllegalStateException(e); - } - - Request request = - new Request.Builder() - .url( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_SEGMENTATION_API_DOMAIN, - FIREBASE_SEGMENTATION_API_VERSION, - resourceName, - apiKey)) - .header("Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken) - .header("Content-Type", "application/json") - .post(requestBody) - .build(); + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty( + "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); + httpsURLConnection.addRequestProperty("Content-Type", "application/json"); + OutputStream os = httpsURLConnection.getOutputStream(); + try { + os.write( + buildClearCustomSegmentationDataRequestBody(resourceName).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } + httpsURLConnection.connect(); - try { - Response response = httpClient.newCall(request).execute(); - switch (response.code()) { + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { case 200: return Code.OK; case 401: From 8b39c3170189b33d92c3646f89cb0dd62de713a0 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 18 Jul 2019 12:04:22 -0700 Subject: [PATCH 32/74] Use gzip for compressing content and fix ourput stream memory leak risk. --- .../remote/SegmentationServiceClient.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 042afef6fc2..03956abd5e8 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -16,8 +16,8 @@ import androidx.annotation.NonNull; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; +import java.util.zip.GZIPOutputStream; import javax.net.ssl.HttpsURLConnection; import org.json.JSONException; import org.json.JSONObject; @@ -33,6 +33,11 @@ public class SegmentationServiceClient { "projects/%s/installations/%s/customSegmentationData:clear"; private static final String FIREBASE_SEGMENTATION_API_VERSION = "v1alpha"; + private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; + private static final String GZIP_CONTENT_ENCODING = "gzip"; + public enum Code { OK, @@ -71,18 +76,21 @@ public Code updateCustomInstallationId( httpsURLConnection.setRequestMethod("PATCH"); httpsURLConnection.addRequestProperty( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); - httpsURLConnection.addRequestProperty("Content-Type", "application/json"); - OutputStream os = httpsURLConnection.getOutputStream(); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { - os.write( + gzipOutputStream.write( buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) .toString() .getBytes("UTF-8")); } catch (JSONException e) { throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); } httpsURLConnection.connect(); - int httpResponseCode = httpsURLConnection.getResponseCode(); switch (httpResponseCode) { case 200: @@ -130,13 +138,17 @@ public Code clearCustomInstallationId( httpsURLConnection.setRequestMethod("POST"); httpsURLConnection.addRequestProperty( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); - httpsURLConnection.addRequestProperty("Content-Type", "application/json"); - OutputStream os = httpsURLConnection.getOutputStream(); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { - os.write( + gzipOutputStream.write( buildClearCustomSegmentationDataRequestBody(resourceName).toString().getBytes("UTF-8")); } catch (JSONException e) { throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); } httpsURLConnection.connect(); From dc1a63c5f7af6a383fbc6c164bb3745c8e1fbbaf Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 22 Jul 2019 11:42:46 -0700 Subject: [PATCH 33/74] Addressed a few comments --- .../segmentation/FirebaseSegmentation.java | 260 ++++++++---------- .../remote/SegmentationServiceClient.java | 3 +- 2 files changed, 114 insertions(+), 149 deletions(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 12b5ff52b88..2008ea09359 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -16,10 +16,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.iid.FirebaseInstanceId; @@ -29,6 +28,7 @@ import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; import com.google.firebase.segmentation.remote.SegmentationServiceClient; import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -42,14 +42,13 @@ public class FirebaseSegmentation { private final Executor executor; FirebaseSegmentation(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); - this.localCache = new CustomInstallationIdCache(firebaseApp); - this.backendServiceClient = new SegmentationServiceClient(); - this.executor = Executors.newFixedThreadPool(4); + this( + firebaseApp, + FirebaseInstanceId.getInstance(firebaseApp), + new CustomInstallationIdCache(firebaseApp), + new SegmentationServiceClient()); } - @RestrictTo(RestrictTo.Scope.TESTS) FirebaseSegmentation( FirebaseApp firebaseApp, FirebaseInstanceId firebaseInstanceId, @@ -88,9 +87,9 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { @NonNull public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { if (customInstallationId == null) { - return clearCustomInstallationId(); + return Tasks.call(executor, () -> clearCustomInstallationId()); } - return updateCustomInstallationId(customInstallationId); + return Tasks.call(executor, () -> updateCustomInstallationId(customInstallationId)); } /** @@ -112,89 +111,73 @@ public synchronized Task setCustomInstallationId(@Nullable String customIn * return * */ - private Task updateCustomInstallationId(String customInstallationId) { - TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - - executor.execute( - () -> { - CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); - if (cacheEntryValue != null - && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) - && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { - // If the given custom installation id matches up the cached - // value, there's no need to update. - taskCompletionSource.setResult(null); - return; - } - - InstanceIdResult instanceIdResult; - try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); - } catch (Exception e) { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR)); - return; - } + @WorkerThread + private Void updateCustomInstallationId(String customInstallationId) + throws SetCustomInstallationIdException { + CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); + if (cacheEntryValue != null + && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) + && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { + // If the given custom installation id matches up the cached + // value, there's no need to update. + return null; + } - boolean firstUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (ExecutionException | InterruptedException e) { + throw new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR); + } - if (!firstUpdateCacheResult) { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR)); - return; - } + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); - // Start requesting backend when first cache updae is done. - String iid = instanceIdResult.getId(); - String iidToken = instanceIdResult.getToken(); - Code backendRequestResult = - backendServiceClient.updateCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - customInstallationId, - iid, - iidToken); + if (!firstUpdateCacheResult) { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } - boolean finalUpdateCacheResult; - switch (backendRequestResult) { - case OK: - finalUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.SYNCED)); - break; - case HTTP_CLIENT_ERROR: - taskCompletionSource.setException( - new SetCustomInstallationIdException(Status.CLIENT_ERROR)); - return; - case CONFLICT: - taskCompletionSource.setException( - new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID)); - return; - default: - taskCompletionSource.setException( - new SetCustomInstallationIdException(Status.BACKEND_ERROR)); - return; - } + // Start requesting backend when first cache updae is done. + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.updateCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + customInstallationId, + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.SYNCED)); + break; + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + case CONFLICT: + throw new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } - if (finalUpdateCacheResult) { - taskCompletionSource.setResult(null); - } else { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR)); - } - }); - return taskCompletionSource.getTask(); + if (finalUpdateCacheResult) { + return null; + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } } /** @@ -214,68 +197,51 @@ private Task updateCustomInstallationId(String customInstallationId) { * return * */ - private Task clearCustomInstallationId() { - - TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - - executor.execute( - () -> { - InstanceIdResult instanceIdResult; - try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); - } catch (Exception e) { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR)); - return; - } - - boolean firstUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "", - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); + @WorkerThread + private Void clearCustomInstallationId() throws SetCustomInstallationIdException { + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (ExecutionException | InterruptedException e) { + throw new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR); + } - if (!firstUpdateCacheResult) { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR)); - return; - } + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "", instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); - String iid = instanceIdResult.getId(); - String iidToken = instanceIdResult.getToken(); - Code backendRequestResult = - backendServiceClient.clearCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - iid, - iidToken); + if (!firstUpdateCacheResult) { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } - boolean finalUpdateCacheResult; - switch (backendRequestResult) { - case OK: - finalUpdateCacheResult = localCache.clear(); - break; - case HTTP_CLIENT_ERROR: - taskCompletionSource.setException( - new SetCustomInstallationIdException(Status.CLIENT_ERROR)); - return; - default: - taskCompletionSource.setException( - new SetCustomInstallationIdException(Status.BACKEND_ERROR)); - return; - } + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.clearCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = localCache.clear(); + break; + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } - if (finalUpdateCacheResult) { - taskCompletionSource.setResult(null); - } else { - taskCompletionSource.setException( - new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR)); - } - }); - return taskCompletionSource.getTask(); + if (finalUpdateCacheResult) { + return null; + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 03956abd5e8..86b5f945205 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -90,7 +90,7 @@ public Code updateCustomInstallationId( } finally { gzipOutputStream.close(); } - httpsURLConnection.connect(); + int httpResponseCode = httpsURLConnection.getResponseCode(); switch (httpResponseCode) { case 200: @@ -150,7 +150,6 @@ public Code clearCustomInstallationId( } finally { gzipOutputStream.close(); } - httpsURLConnection.connect(); int httpResponseCode = httpsURLConnection.getResponseCode(); switch (httpResponseCode) { From d07ec25295de8298f4e18f68b03e2758abf44b76 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Mon, 22 Jul 2019 12:39:55 -0700 Subject: [PATCH 34/74] Http client in Firebase Segmentation SDK to call backend service. (#573) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit * Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) * minor format fix * Address comments #1 * Http client in Firebase Segmentation SDK to call backend service. * Revert unintentional change * Fix connected device test * Fix connected device test * 1. Add a few annotations to make java code Kotlin friendly 2. Some fixes for the http request format * Fix java format * Fix API version * Change the segmentation API implementation to synchronous and put the entire synchronous code block in async task. * Fix a async getResult race issue. * OkHttpClient -> HttpsUrlConnection * Use gzip for compressing content and fix ourput stream memory leak risk. * Addressed a few comments --- .../firebase-segmentation.gradle | 52 +--- .../FirebaseSegmentationInstrumentedTest.java | 45 ++-- .../local/CustomInstallationIdCacheTest.java | 21 +- .../segmentation/FirebaseSegmentation.java | 227 +++++++++--------- .../FirebaseSegmentationRegistrar.java | 2 + .../local/CustomInstallationIdCache.java | 27 +-- .../CustomInstallationIdCacheEntryValue.java | 9 +- .../remote/SegmentationServiceClient.java | 158 ++++++++++-- 8 files changed, 309 insertions(+), 232 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 1f76b8b593a..c11085bd161 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -14,45 +14,12 @@ plugins { id 'firebase-library' - id 'com.google.protobuf' } firebaseLibrary { testLab.enabled = true } -protobuf { - // Configure the protoc executable - protoc { - // Download from repositories - artifact = 'com.google.protobuf:protoc:3.4.0' - } - plugins { - grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.12.0' - } - javalite { - // The codegen for lite comes as a separate artifact - artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - // In most cases you don't need the full Java output - // if you use the lite output. - remove java - } - task.plugins { - grpc { - option 'lite' - } - javalite {} - } - } - } -} - android { compileSdkVersion project.targetSdkVersion @@ -63,13 +30,6 @@ android { versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - sourceSets { - main { - proto { - srcDir 'src/main/proto' - } - } - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -83,18 +43,14 @@ android { dependencies { implementation project(':firebase-common') - implementation project(':protolite-well-known-types') implementation('com.google.firebase:firebase-iid:17.0.3') { exclude group: "com.google.firebase", module: "firebase-common" } - implementation 'io.grpc:grpc-stub:1.21.0' - implementation 'io.grpc:grpc-protobuf-lite:1.21.0' - implementation 'io.grpc:grpc-okhttp:1.21.0' + implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.multidex:multidex:2.0.0' - implementation 'com.google.android.gms:play-services-tasks:16.0.1' - implementation 'com.squareup.okhttp:okhttp:2.7.5' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" @@ -103,7 +59,7 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" - androidTestImplementation "androidx.annotation:annotation:1.1.0" + androidTestImplementation "androidx.annotation:annotation:1.0.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 8519e641cb7..19498782a78 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java @@ -69,19 +69,24 @@ public void setUp() { firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + new FirebaseOptions.Builder() + .setApplicationId("1:123456789:android:abcdef") + .setApiKey("api_key") + .build()); actualCache = new CustomInstallationIdCache(firebaseApp); when(backendClientReturnsOk.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(SegmentationServiceClient.Code.OK); + when(backendClientReturnsOk.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); - when(backendClientReturnsOk.clearCustomInstallationId(anyLong(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.OK)); + .thenReturn(SegmentationServiceClient.Code.OK); when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); + when(backendClientReturnsError.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); - when(backendClientReturnsError.clearCustomInstallationId(anyLong(), anyString(), anyString())) - .thenReturn(Tasks.forResult(SegmentationServiceClient.Code.SERVER_INTERNAL_ERROR)); + .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); when(firebaseInstanceId.getInstanceId()) .thenReturn( Tasks.forResult( @@ -98,13 +103,13 @@ public String getToken() { return "iid_token"; } })); - when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(Tasks.forResult(false)); + when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false); when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); } @After public void cleanUp() throws Exception { - Tasks.await(actualCache.clear()); + actualCache.clear(); } @Test @@ -165,12 +170,11 @@ public void testUpdateCustomInstallationId_CacheError_BackendOk() throws Interru @Test public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { - Tasks.await( - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED))); + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); @@ -183,12 +187,11 @@ public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { @Test public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { - Tasks.await( - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED))); + actualCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + CUSTOM_INSTALLATION_ID, + FIREBASE_INSTANCE_ID, + CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 019a2b8ba08..9e22e522d2b 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -20,7 +20,6 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import org.junit.After; @@ -55,8 +54,8 @@ public void setUp() { @After public void cleanUp() throws Exception { - Tasks.await(cache0.clear()); - Tasks.await(cache1.clear()); + cache0.clear(); + cache1.clear(); } @Test @@ -68,12 +67,9 @@ public void testReadCacheEntry_Null() { @Test public void testUpdateAndReadCacheEntry() throws Exception { assertTrue( - Tasks.await( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", - "cAAAAAAAAAA", - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)))); + cache0.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); @@ -82,10 +78,9 @@ public void testUpdateAndReadCacheEntry() throws Exception { assertNull(cache1.readCacheEntryValue()); assertTrue( - Tasks.await( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); + cache0.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED))); entryValue = cache0.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 34de68597c1..2008ea09359 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -16,7 +16,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; @@ -28,6 +28,9 @@ import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; import com.google.firebase.segmentation.remote.SegmentationServiceClient; import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { @@ -36,15 +39,16 @@ public class FirebaseSegmentation { private final FirebaseInstanceId firebaseInstanceId; private final CustomInstallationIdCache localCache; private final SegmentationServiceClient backendServiceClient; + private final Executor executor; FirebaseSegmentation(FirebaseApp firebaseApp) { - this.firebaseApp = firebaseApp; - this.firebaseInstanceId = FirebaseInstanceId.getInstance(firebaseApp); - localCache = new CustomInstallationIdCache(firebaseApp); - backendServiceClient = new SegmentationServiceClient(); + this( + firebaseApp, + FirebaseInstanceId.getInstance(firebaseApp), + new CustomInstallationIdCache(firebaseApp), + new SegmentationServiceClient()); } - @RestrictTo(RestrictTo.Scope.TESTS) FirebaseSegmentation( FirebaseApp firebaseApp, FirebaseInstanceId firebaseInstanceId, @@ -54,6 +58,7 @@ public class FirebaseSegmentation { this.firebaseInstanceId = firebaseInstanceId; this.localCache = localCache; this.backendServiceClient = backendServiceClient; + this.executor = Executors.newFixedThreadPool(4); } /** @@ -82,9 +87,9 @@ public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { @NonNull public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { if (customInstallationId == null) { - return clearCustomInstallationId(); + return Tasks.call(executor, () -> clearCustomInstallationId()); } - return updateCustomInstallationId(customInstallationId); + return Tasks.call(executor, () -> updateCustomInstallationId(customInstallationId)); } /** @@ -106,71 +111,73 @@ public synchronized Task setCustomInstallationId(@Nullable String customIn * return * */ - private Task updateCustomInstallationId(String customInstallationId) { + @WorkerThread + private Void updateCustomInstallationId(String customInstallationId) + throws SetCustomInstallationIdException { CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); if (cacheEntryValue != null && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { // If the given custom installation id matches up the cached // value, there's no need to update. - return Tasks.forResult(null); + return null; + } + + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (ExecutionException | InterruptedException e) { + throw new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR); } - Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); - Task firstUpdateCacheResultTask = - instanceIdResultTask.onSuccessTask( - instanceIdResult -> - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); - - // Start requesting backend when first cache update is done. - Task backendRequestResultTask = - firstUpdateCacheResultTask.onSuccessTask( - firstUpdateCacheResult -> { - if (firstUpdateCacheResult) { - String iid = instanceIdResultTask.getResult().getId(); - String iidToken = instanceIdResultTask.getResult().getToken(); - return backendServiceClient.updateCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + customInstallationId, + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); + + if (!firstUpdateCacheResult) { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + + // Start requesting backend when first cache updae is done. + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.updateCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + customInstallationId, + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( customInstallationId, - iid, - iidToken); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); - - Task finalUpdateCacheResultTask = - backendRequestResultTask.onSuccessTask( - backendRequestResult -> { - switch (backendRequestResult) { - case OK: - return localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResultTask.getResult().getId(), - CustomInstallationIdCache.CacheStatus.SYNCED)); - case ALREADY_EXISTS: - throw new SetCustomInstallationIdException( - Status.DUPLICATED_CUSTOM_INSTALLATION_ID); - default: - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - }); - - return finalUpdateCacheResultTask.onSuccessTask( - finalUpdateCacheResult -> { - if (finalUpdateCacheResult) { - return Tasks.forResult(null); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); + instanceIdResult.getId(), + CustomInstallationIdCache.CacheStatus.SYNCED)); + break; + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + case CONFLICT: + throw new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + + if (finalUpdateCacheResult) { + return null; + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } } /** @@ -190,51 +197,51 @@ private Task updateCustomInstallationId(String customInstallationId) { * return * */ - private Task clearCustomInstallationId() { - Task instanceIdResultTask = firebaseInstanceId.getInstanceId(); - Task firstUpdateCacheResultTask = - instanceIdResultTask.onSuccessTask( - instanceIdResult -> - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "", - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_CLEAR))); - - Task backendRequestResultTask = - firstUpdateCacheResultTask.onSuccessTask( - firstUpdateCacheResult -> { - if (firstUpdateCacheResult) { - String iid = instanceIdResultTask.getResult().getId(); - String iidToken = instanceIdResultTask.getResult().getToken(); - return backendServiceClient.clearCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - iid, - iidToken); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); - - Task finalUpdateCacheResultTask = - backendRequestResultTask.onSuccessTask( - backendRequestResult -> { - if (backendRequestResult == Code.OK) { - return localCache.clear(); - } else { - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - }); - - return finalUpdateCacheResultTask.onSuccessTask( - finalUpdateCacheResult -> { - if (finalUpdateCacheResult) { - return Tasks.forResult(null); - } else { - throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); - } - }); + @WorkerThread + private Void clearCustomInstallationId() throws SetCustomInstallationIdException { + InstanceIdResult instanceIdResult; + try { + instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + } catch (ExecutionException | InterruptedException e) { + throw new SetCustomInstallationIdException( + "Failed to get Firebase instance id", Status.CLIENT_ERROR); + } + + boolean firstUpdateCacheResult = + localCache.insertOrUpdateCacheEntry( + CustomInstallationIdCacheEntryValue.create( + "", instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); + + if (!firstUpdateCacheResult) { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } + + String iid = instanceIdResult.getId(); + String iidToken = instanceIdResult.getToken(); + Code backendRequestResult = + backendServiceClient.clearCustomInstallationId( + Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), + firebaseApp.getOptions().getApiKey(), + iid, + iidToken); + + boolean finalUpdateCacheResult; + switch (backendRequestResult) { + case OK: + finalUpdateCacheResult = localCache.clear(); + break; + case HTTP_CLIENT_ERROR: + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + default: + throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); + } + + if (finalUpdateCacheResult) { + return null; + } else { + throw new SetCustomInstallationIdException( + "Failed to update client side cache", Status.CLIENT_ERROR); + } } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java index 7d1d5fcfaab..ca7f688d60a 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java @@ -14,6 +14,7 @@ package com.google.firebase.segmentation; +import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; @@ -25,6 +26,7 @@ public class FirebaseSegmentationRegistrar implements ComponentRegistrar { @Override + @NonNull public List> getComponents() { return Arrays.asList( Component.builder(FirebaseSegmentation.class) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java index a1346062440..307d5d49923 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java @@ -16,12 +16,9 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -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; /** * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation @@ -49,11 +46,10 @@ public enum CacheStatus { private static final String INSTANCE_ID_KEY = "Iid"; private static final String CACHE_STATUS_KEY = "Status"; - private final Executor ioExecuter; private final SharedPreferences prefs; private final String persistenceKey; - public CustomInstallationIdCache(FirebaseApp firebaseApp) { + public CustomInstallationIdCache(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path prefs = @@ -61,7 +57,6 @@ public CustomInstallationIdCache(FirebaseApp firebaseApp) { .getApplicationContext() .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); persistenceKey = firebaseApp.getPersistenceKey(); - ioExecuter = Executors.newFixedThreadPool(2); } @Nullable @@ -77,31 +72,27 @@ public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } - public synchronized Task insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue entryValue) { + @NonNull + public synchronized boolean insertOrUpdateCacheEntry( + @NonNull CustomInstallationIdCacheEntryValue entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); - return commitSharedPreferencesEditAsync(editor); + return editor.commit(); } - public synchronized Task clear() { + @NonNull + public synchronized boolean clear() { SharedPreferences.Editor editor = prefs.edit(); editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY)); editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); - return commitSharedPreferencesEditAsync(editor); + return editor.commit(); } private String getSharedPreferencesKey(String key) { return String.format("%s|%s", persistenceKey, key); } - - private Task commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { - TaskCompletionSource result = new TaskCompletionSource<>(); - ioExecuter.execute(() -> result.setResult(editor.commit())); - return result.getTask(); - } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java index 05528cd40f0..5e2c1944278 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java @@ -14,6 +14,7 @@ package com.google.firebase.segmentation.local; +import androidx.annotation.NonNull; import com.google.auto.value.AutoValue; import com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus; @@ -23,14 +24,20 @@ */ @AutoValue public abstract class CustomInstallationIdCacheEntryValue { + @NonNull public abstract String getCustomInstallationId(); + @NonNull public abstract String getFirebaseInstanceId(); + @NonNull public abstract CacheStatus getCacheStatus(); + @NonNull public static CustomInstallationIdCacheEntryValue create( - String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { + @NonNull String customInstallationId, + @NonNull String firebaseInstanceId, + @NonNull CacheStatus cacheStatus) { return new AutoValue_CustomInstallationIdCacheEntryValue( customInstallationId, firebaseInstanceId, cacheStatus); } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 59398b369d5..86b5f945205 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -14,43 +14,159 @@ package com.google.firebase.segmentation.remote; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.squareup.okhttp.OkHttpClient; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import androidx.annotation.NonNull; +import java.io.IOException; +import java.net.URL; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.json.JSONException; +import org.json.JSONObject; /** Http client that sends request to Firebase Segmentation backend API. To be implemented */ public class SegmentationServiceClient { - private final OkHttpClient httpClient; - private final Executor httpRequestExecutor; + private static final String FIREBASE_SEGMENTATION_API_DOMAIN = + "firebasesegmentation.googleapis.com"; + private static final String UPDATE_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/customSegmentationData"; + private static final String CLEAR_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/customSegmentationData:clear"; + private static final String FIREBASE_SEGMENTATION_API_VERSION = "v1alpha"; + + private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; + private static final String GZIP_CONTENT_ENCODING = "gzip"; public enum Code { OK, - SERVER_INTERNAL_ERROR, + HTTP_CLIENT_ERROR, + + CONFLICT, + + NETWORK_ERROR, + + SERVER_ERROR, + + UNAUTHORIZED, + } + + @NonNull + public Code updateCustomInstallationId( + long projectNumber, + @NonNull String apiKey, + @NonNull String customInstallationId, + @NonNull String firebaseInstanceId, + @NonNull String firebaseInstanceIdToken) { + String resourceName = + String.format(UPDATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName, + apiKey)); - ALREADY_EXISTS, + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("PATCH"); + httpsURLConnection.addRequestProperty( + "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) + .toString() + .getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } - PERMISSION_DENIED + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return Code.OK; + case 401: + return Code.UNAUTHORIZED; + case 409: + return Code.CONFLICT; + default: + return Code.SERVER_ERROR; + } + } catch (IOException e) { + return Code.NETWORK_ERROR; + } } - public SegmentationServiceClient() { - httpClient = new OkHttpClient(); - httpRequestExecutor = Executors.newFixedThreadPool(4); + private static JSONObject buildUpdateCustomSegmentationDataRequestBody( + String resourceName, String customInstallationId) throws JSONException { + JSONObject customSegmentationData = new JSONObject(); + customSegmentationData.put("name", resourceName); + customSegmentationData.put("custom_installation_id", customInstallationId); + return customSegmentationData; } - public Task updateCustomInstallationId( + @NonNull + public Code clearCustomInstallationId( long projectNumber, - String customInstallationId, - String firebaseInstanceId, - String firebaseInstanceIdToken) { - return Tasks.forResult(Code.OK); + @NonNull String apiKey, + @NonNull String firebaseInstanceId, + @NonNull String firebaseInstanceIdToken) { + String resourceName = + String.format(CLEAR_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_SEGMENTATION_API_DOMAIN, + FIREBASE_SEGMENTATION_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty( + "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildClearCustomSegmentationDataRequestBody(resourceName).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return Code.OK; + case 401: + return Code.UNAUTHORIZED; + default: + return Code.SERVER_ERROR; + } + } catch (IOException e) { + return Code.NETWORK_ERROR; + } } - public Task clearCustomInstallationId( - long projectNumber, String firebaseInstanceId, String firebaseInstanceIdToken) { - return Tasks.forResult(Code.OK); + private static JSONObject buildClearCustomSegmentationDataRequestBody(String resourceName) + throws JSONException { + return new JSONObject().put("name", resourceName); } } From 64af341833e39c53e2351ccfcbf2eae76e1e2c74 Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 24 Jul 2019 11:24:14 -0700 Subject: [PATCH 35/74] Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. --- .../firebase-installations-interop.gradle | 46 ++++++++ .../gradle.properties | 1 + .../src/main/AndroidManifest.xml | 17 +++ .../FirebaseInstallationsApi.java | 42 +++++++ .../InstallationTokenResult.java | 38 +++++++ .../firebase-installations.gradle | 54 +++++++++ firebase-installations/gradle.properties | 1 + firebase-installations/lint.xml | 11 ++ .../src/main/AndroidManifest.xml | 14 +++ .../installations/FirebaseInstallations.java | 106 ++++++++++++++++++ .../FirebaseInstallationsRegistrar.java | 39 +++++++ .../FirebaseInstallationsRegistrarTest.java | 18 +++ .../FirebaseInstallationsTest.java | 18 +++ subprojects.cfg | 2 + 14 files changed, 407 insertions(+) create mode 100644 firebase-installations-interop/firebase-installations-interop.gradle create mode 100644 firebase-installations-interop/gradle.properties create mode 100644 firebase-installations-interop/src/main/AndroidManifest.xml create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java create mode 100644 firebase-installations/firebase-installations.gradle create mode 100644 firebase-installations/gradle.properties create mode 100644 firebase-installations/lint.xml create mode 100644 firebase-installations/src/main/AndroidManifest.xml create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java diff --git a/firebase-installations-interop/firebase-installations-interop.gradle b/firebase-installations-interop/firebase-installations-interop.gradle new file mode 100644 index 00000000000..5acad3e1434 --- /dev/null +++ b/firebase-installations-interop/firebase-installations-interop.gradle @@ -0,0 +1,46 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +firebaseLibrary.publishJavadoc = false + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" +} diff --git a/firebase-installations-interop/gradle.properties b/firebase-installations-interop/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations-interop/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations-interop/src/main/AndroidManifest.xml b/firebase-installations-interop/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7ae18eafe43 --- /dev/null +++ b/firebase-installations-interop/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java new file mode 100644 index 00000000000..6947d7ec037 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -0,0 +1,42 @@ +// 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.installations; + +import com.google.android.gms.tasks.Task; + +/** + * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component + * injection. + * + * @hide + */ +public interface FirebaseInstallationsApi { + + /** + * Async function that returns a globally unique identifier of this Firebase app installation. + * This is a url-safe base64 string of a 128-bit integer. + */ + Task getId(); + + /** Async function that returns a auth token(public key) of this Firebase app installation. */ + Task getAuthToken(boolean forceRefresh); + + /** + * Async function that deletes this Firebase app installation from Firebase backend. This call + * would possibly lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or + * Firebase In-App Messaging not function properly. + */ + Task delete(); +} diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java new file mode 100644 index 00000000000..df04f26ca38 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -0,0 +1,38 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class InstallationTokenResult { + + /** A new FIS Auth-Token, created for this firebase installation. */ + @NonNull + public abstract String getAuthToken(); + /** + * The amount of time, in milliseconds, before the auth-token expires for this firebase + * installation. + */ + public abstract long getTokenExpirationTimestampMillis(); + + @NonNull + public static InstallationTokenResult create( + @NonNull String authToken, long tokenExpirationTimestampMillis) { + return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + } +} diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle new file mode 100644 index 00000000000..45123f1c6ce --- /dev/null +++ b/firebase-installations/firebase-installations.gradle @@ -0,0 +1,54 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + multiDexEnabled true + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':firebase-common') + implementation project(':firebase-installations-interop') + + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + testImplementation 'androidx.test:core:1.2.0' + testImplementation 'junit:junit:4.12' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + + androidTestImplementation 'androidx.test:runner:1.2.0' + implementation 'com.google.guava:guava:16.0.+' +} diff --git a/firebase-installations/gradle.properties b/firebase-installations/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations/lint.xml b/firebase-installations/lint.xml new file mode 100644 index 00000000000..9b9bd90b534 --- /dev/null +++ b/firebase-installations/lint.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..08b562940aa --- /dev/null +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java new file mode 100644 index 00000000000..f135aa2e197 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -0,0 +1,106 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; + +/** + * Entry point for Firebase Installations. + * + *

Firebase Installations does + * + *

    + *
  • provide unique identifier for a Firebase installation + *
  • provide auth token of a Firebase installation + *
  • provide a API to GDPR-delete a Firebase installation + *
+ */ +public class FirebaseInstallations implements FirebaseInstallationsApi { + + private final FirebaseApp firebaseApp; + + /** package private constructor. */ + FirebaseInstallations(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + } + + /** + * Returns the {@link FirebaseInstallationsApi} initialized with the default {@link FirebaseApp}. + * + * @return a {@link FirebaseInstallationsApi} instance + */ + @NonNull + public static FirebaseInstallations getInstance() { + FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); + return getInstance(defaultFirebaseApp); + } + + /** + * Returns the {@link FirebaseInstallations} initialized with a custom {@link FirebaseApp}. + * + * @param app a custom {@link FirebaseApp} + * @return a {@link FirebaseInstallations} instance + */ + @NonNull + public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + return (FirebaseInstallations) app.get(FirebaseInstallationsApi.class); + } + + /** + * Returns a globally unique identifier of this Firebase app installation. This is a url-safe + * base64 string of a 128-bit integer. + */ + @NonNull + @Override + public Task getId() { + return Tasks.forResult("fid-is-better-than-iid"); + } + + /** Returns a auth token(public key) of this Firebase app installation. */ + @NonNull + @Override + public Task getAuthToken(boolean forceRefresh) { + return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + } + + /** + * Call to delete this Firebase app installation from Firebase backend. This call would possibly + * lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or Firebase In-App + * Messaging not function properly. + */ + @NonNull + @Override + public Task delete() { + return Tasks.forResult(null); + } + + /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getApplicationId() { + return firebaseApp.getOptions().getApplicationId(); + } + + /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getName() { + return firebaseApp.getName(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java new file mode 100644 index 00000000000..e84168ab4fb --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java @@ -0,0 +1,39 @@ +// 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.installations; + +import androidx.annotation.Keep; +import com.google.firebase.FirebaseApp; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** @hide */ +@Keep +public class FirebaseInstallationsRegistrar implements ComponentRegistrar { + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseInstallationsApi.class) + .add(Dependency.required(FirebaseApp.class)) + .factory(c -> new FirebaseInstallations(c.get(FirebaseApp.class))) + .build(), + LibraryVersionComponent.create("fire-installations", BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java new file mode 100644 index 00000000000..5a7c505e9b6 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallationsRegistrar}. */ +public class FirebaseInstallationsRegistrarTest {} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java new file mode 100644 index 00000000000..fe64dcf06b8 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallations}. */ +public class FirebaseInstallationsTest {} diff --git a/subprojects.cfg b/subprojects.cfg index a80ebdb3c42..031fb302706 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -12,6 +12,8 @@ firebase-firestore firebase-firestore:ktx firebase-functions firebase-functions:ktx +firebase-installations-interop +firebase-installations firebase-inappmessaging-display firebase-storage firebase-storage:test-app From beccce48891db24ec39db58451af34e412613606 Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 24 Jul 2019 11:24:14 -0700 Subject: [PATCH 36/74] Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. --- .../firebase-installations-interop.gradle | 46 ++++++++ .../gradle.properties | 1 + .../src/main/AndroidManifest.xml | 17 +++ .../FirebaseInstallationsApi.java | 42 +++++++ .../InstallationTokenResult.java | 38 +++++++ .../firebase-installations.gradle | 54 +++++++++ firebase-installations/gradle.properties | 1 + firebase-installations/lint.xml | 11 ++ .../src/main/AndroidManifest.xml | 14 +++ .../installations/FirebaseInstallations.java | 106 ++++++++++++++++++ .../FirebaseInstallationsRegistrar.java | 39 +++++++ .../FirebaseInstallationsRegistrarTest.java | 18 +++ .../FirebaseInstallationsTest.java | 18 +++ subprojects.cfg | 2 + 14 files changed, 407 insertions(+) create mode 100644 firebase-installations-interop/firebase-installations-interop.gradle create mode 100644 firebase-installations-interop/gradle.properties create mode 100644 firebase-installations-interop/src/main/AndroidManifest.xml create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java create mode 100644 firebase-installations/firebase-installations.gradle create mode 100644 firebase-installations/gradle.properties create mode 100644 firebase-installations/lint.xml create mode 100644 firebase-installations/src/main/AndroidManifest.xml create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java diff --git a/firebase-installations-interop/firebase-installations-interop.gradle b/firebase-installations-interop/firebase-installations-interop.gradle new file mode 100644 index 00000000000..5acad3e1434 --- /dev/null +++ b/firebase-installations-interop/firebase-installations-interop.gradle @@ -0,0 +1,46 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +firebaseLibrary.publishJavadoc = false + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" +} diff --git a/firebase-installations-interop/gradle.properties b/firebase-installations-interop/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations-interop/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations-interop/src/main/AndroidManifest.xml b/firebase-installations-interop/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7ae18eafe43 --- /dev/null +++ b/firebase-installations-interop/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java new file mode 100644 index 00000000000..6947d7ec037 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -0,0 +1,42 @@ +// 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.installations; + +import com.google.android.gms.tasks.Task; + +/** + * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component + * injection. + * + * @hide + */ +public interface FirebaseInstallationsApi { + + /** + * Async function that returns a globally unique identifier of this Firebase app installation. + * This is a url-safe base64 string of a 128-bit integer. + */ + Task getId(); + + /** Async function that returns a auth token(public key) of this Firebase app installation. */ + Task getAuthToken(boolean forceRefresh); + + /** + * Async function that deletes this Firebase app installation from Firebase backend. This call + * would possibly lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or + * Firebase In-App Messaging not function properly. + */ + Task delete(); +} diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java new file mode 100644 index 00000000000..df04f26ca38 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -0,0 +1,38 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class InstallationTokenResult { + + /** A new FIS Auth-Token, created for this firebase installation. */ + @NonNull + public abstract String getAuthToken(); + /** + * The amount of time, in milliseconds, before the auth-token expires for this firebase + * installation. + */ + public abstract long getTokenExpirationTimestampMillis(); + + @NonNull + public static InstallationTokenResult create( + @NonNull String authToken, long tokenExpirationTimestampMillis) { + return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + } +} diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle new file mode 100644 index 00000000000..45123f1c6ce --- /dev/null +++ b/firebase-installations/firebase-installations.gradle @@ -0,0 +1,54 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + multiDexEnabled true + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':firebase-common') + implementation project(':firebase-installations-interop') + + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + testImplementation 'androidx.test:core:1.2.0' + testImplementation 'junit:junit:4.12' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + + androidTestImplementation 'androidx.test:runner:1.2.0' + implementation 'com.google.guava:guava:16.0.+' +} diff --git a/firebase-installations/gradle.properties b/firebase-installations/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations/lint.xml b/firebase-installations/lint.xml new file mode 100644 index 00000000000..9b9bd90b534 --- /dev/null +++ b/firebase-installations/lint.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..08b562940aa --- /dev/null +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java new file mode 100644 index 00000000000..f135aa2e197 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -0,0 +1,106 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; + +/** + * Entry point for Firebase Installations. + * + *

Firebase Installations does + * + *

    + *
  • provide unique identifier for a Firebase installation + *
  • provide auth token of a Firebase installation + *
  • provide a API to GDPR-delete a Firebase installation + *
+ */ +public class FirebaseInstallations implements FirebaseInstallationsApi { + + private final FirebaseApp firebaseApp; + + /** package private constructor. */ + FirebaseInstallations(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + } + + /** + * Returns the {@link FirebaseInstallationsApi} initialized with the default {@link FirebaseApp}. + * + * @return a {@link FirebaseInstallationsApi} instance + */ + @NonNull + public static FirebaseInstallations getInstance() { + FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); + return getInstance(defaultFirebaseApp); + } + + /** + * Returns the {@link FirebaseInstallations} initialized with a custom {@link FirebaseApp}. + * + * @param app a custom {@link FirebaseApp} + * @return a {@link FirebaseInstallations} instance + */ + @NonNull + public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + return (FirebaseInstallations) app.get(FirebaseInstallationsApi.class); + } + + /** + * Returns a globally unique identifier of this Firebase app installation. This is a url-safe + * base64 string of a 128-bit integer. + */ + @NonNull + @Override + public Task getId() { + return Tasks.forResult("fid-is-better-than-iid"); + } + + /** Returns a auth token(public key) of this Firebase app installation. */ + @NonNull + @Override + public Task getAuthToken(boolean forceRefresh) { + return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + } + + /** + * Call to delete this Firebase app installation from Firebase backend. This call would possibly + * lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or Firebase In-App + * Messaging not function properly. + */ + @NonNull + @Override + public Task delete() { + return Tasks.forResult(null); + } + + /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getApplicationId() { + return firebaseApp.getOptions().getApplicationId(); + } + + /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getName() { + return firebaseApp.getName(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java new file mode 100644 index 00000000000..e84168ab4fb --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java @@ -0,0 +1,39 @@ +// 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.installations; + +import androidx.annotation.Keep; +import com.google.firebase.FirebaseApp; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** @hide */ +@Keep +public class FirebaseInstallationsRegistrar implements ComponentRegistrar { + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseInstallationsApi.class) + .add(Dependency.required(FirebaseApp.class)) + .factory(c -> new FirebaseInstallations(c.get(FirebaseApp.class))) + .build(), + LibraryVersionComponent.create("fire-installations", BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java new file mode 100644 index 00000000000..5a7c505e9b6 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallationsRegistrar}. */ +public class FirebaseInstallationsRegistrarTest {} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java new file mode 100644 index 00000000000..fe64dcf06b8 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallations}. */ +public class FirebaseInstallationsTest {} diff --git a/subprojects.cfg b/subprojects.cfg index a80ebdb3c42..031fb302706 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -12,6 +12,8 @@ firebase-firestore firebase-firestore:ktx firebase-functions firebase-functions:ktx +firebase-installations-interop +firebase-installations firebase-inappmessaging-display firebase-storage firebase-storage:test-app From d74f799e91b1b90cf90142b121b01cf1c953ad6c Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 24 Jul 2019 11:24:14 -0700 Subject: [PATCH 37/74] Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. --- .../firebase-installations-interop.gradle | 46 ++++++++ .../gradle.properties | 1 + .../src/main/AndroidManifest.xml | 17 +++ .../FirebaseInstallationsApi.java | 42 +++++++ .../InstallationTokenResult.java | 38 +++++++ .../firebase-installations.gradle | 54 +++++++++ firebase-installations/gradle.properties | 1 + firebase-installations/lint.xml | 11 ++ .../src/main/AndroidManifest.xml | 14 +++ .../installations/FirebaseInstallations.java | 106 ++++++++++++++++++ .../FirebaseInstallationsRegistrar.java | 39 +++++++ .../FirebaseInstallationsRegistrarTest.java | 18 +++ .../FirebaseInstallationsTest.java | 18 +++ subprojects.cfg | 2 + 14 files changed, 407 insertions(+) create mode 100644 firebase-installations-interop/firebase-installations-interop.gradle create mode 100644 firebase-installations-interop/gradle.properties create mode 100644 firebase-installations-interop/src/main/AndroidManifest.xml create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java create mode 100644 firebase-installations/firebase-installations.gradle create mode 100644 firebase-installations/gradle.properties create mode 100644 firebase-installations/lint.xml create mode 100644 firebase-installations/src/main/AndroidManifest.xml create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java diff --git a/firebase-installations-interop/firebase-installations-interop.gradle b/firebase-installations-interop/firebase-installations-interop.gradle new file mode 100644 index 00000000000..5acad3e1434 --- /dev/null +++ b/firebase-installations-interop/firebase-installations-interop.gradle @@ -0,0 +1,46 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +firebaseLibrary.publishJavadoc = false + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" +} diff --git a/firebase-installations-interop/gradle.properties b/firebase-installations-interop/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations-interop/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations-interop/src/main/AndroidManifest.xml b/firebase-installations-interop/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7ae18eafe43 --- /dev/null +++ b/firebase-installations-interop/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java new file mode 100644 index 00000000000..6947d7ec037 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -0,0 +1,42 @@ +// 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.installations; + +import com.google.android.gms.tasks.Task; + +/** + * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component + * injection. + * + * @hide + */ +public interface FirebaseInstallationsApi { + + /** + * Async function that returns a globally unique identifier of this Firebase app installation. + * This is a url-safe base64 string of a 128-bit integer. + */ + Task getId(); + + /** Async function that returns a auth token(public key) of this Firebase app installation. */ + Task getAuthToken(boolean forceRefresh); + + /** + * Async function that deletes this Firebase app installation from Firebase backend. This call + * would possibly lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or + * Firebase In-App Messaging not function properly. + */ + Task delete(); +} diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java new file mode 100644 index 00000000000..df04f26ca38 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -0,0 +1,38 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class InstallationTokenResult { + + /** A new FIS Auth-Token, created for this firebase installation. */ + @NonNull + public abstract String getAuthToken(); + /** + * The amount of time, in milliseconds, before the auth-token expires for this firebase + * installation. + */ + public abstract long getTokenExpirationTimestampMillis(); + + @NonNull + public static InstallationTokenResult create( + @NonNull String authToken, long tokenExpirationTimestampMillis) { + return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + } +} diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle new file mode 100644 index 00000000000..45123f1c6ce --- /dev/null +++ b/firebase-installations/firebase-installations.gradle @@ -0,0 +1,54 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + multiDexEnabled true + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':firebase-common') + implementation project(':firebase-installations-interop') + + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + testImplementation 'androidx.test:core:1.2.0' + testImplementation 'junit:junit:4.12' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + + androidTestImplementation 'androidx.test:runner:1.2.0' + implementation 'com.google.guava:guava:16.0.+' +} diff --git a/firebase-installations/gradle.properties b/firebase-installations/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations/lint.xml b/firebase-installations/lint.xml new file mode 100644 index 00000000000..9b9bd90b534 --- /dev/null +++ b/firebase-installations/lint.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..08b562940aa --- /dev/null +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java new file mode 100644 index 00000000000..f135aa2e197 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -0,0 +1,106 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; + +/** + * Entry point for Firebase Installations. + * + *

Firebase Installations does + * + *

    + *
  • provide unique identifier for a Firebase installation + *
  • provide auth token of a Firebase installation + *
  • provide a API to GDPR-delete a Firebase installation + *
+ */ +public class FirebaseInstallations implements FirebaseInstallationsApi { + + private final FirebaseApp firebaseApp; + + /** package private constructor. */ + FirebaseInstallations(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + } + + /** + * Returns the {@link FirebaseInstallationsApi} initialized with the default {@link FirebaseApp}. + * + * @return a {@link FirebaseInstallationsApi} instance + */ + @NonNull + public static FirebaseInstallations getInstance() { + FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); + return getInstance(defaultFirebaseApp); + } + + /** + * Returns the {@link FirebaseInstallations} initialized with a custom {@link FirebaseApp}. + * + * @param app a custom {@link FirebaseApp} + * @return a {@link FirebaseInstallations} instance + */ + @NonNull + public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + return (FirebaseInstallations) app.get(FirebaseInstallationsApi.class); + } + + /** + * Returns a globally unique identifier of this Firebase app installation. This is a url-safe + * base64 string of a 128-bit integer. + */ + @NonNull + @Override + public Task getId() { + return Tasks.forResult("fid-is-better-than-iid"); + } + + /** Returns a auth token(public key) of this Firebase app installation. */ + @NonNull + @Override + public Task getAuthToken(boolean forceRefresh) { + return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + } + + /** + * Call to delete this Firebase app installation from Firebase backend. This call would possibly + * lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or Firebase In-App + * Messaging not function properly. + */ + @NonNull + @Override + public Task delete() { + return Tasks.forResult(null); + } + + /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getApplicationId() { + return firebaseApp.getOptions().getApplicationId(); + } + + /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getName() { + return firebaseApp.getName(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java new file mode 100644 index 00000000000..e84168ab4fb --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java @@ -0,0 +1,39 @@ +// 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.installations; + +import androidx.annotation.Keep; +import com.google.firebase.FirebaseApp; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** @hide */ +@Keep +public class FirebaseInstallationsRegistrar implements ComponentRegistrar { + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseInstallationsApi.class) + .add(Dependency.required(FirebaseApp.class)) + .factory(c -> new FirebaseInstallations(c.get(FirebaseApp.class))) + .build(), + LibraryVersionComponent.create("fire-installations", BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java new file mode 100644 index 00000000000..5a7c505e9b6 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallationsRegistrar}. */ +public class FirebaseInstallationsRegistrarTest {} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java new file mode 100644 index 00000000000..fe64dcf06b8 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallations}. */ +public class FirebaseInstallationsTest {} diff --git a/subprojects.cfg b/subprojects.cfg index a80ebdb3c42..031fb302706 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -12,6 +12,8 @@ firebase-firestore firebase-firestore:ktx firebase-functions firebase-functions:ktx +firebase-installations-interop +firebase-installations firebase-inappmessaging-display firebase-storage firebase-storage:test-app From 866530172c48cbf01c1c928faa32781486b0efcd Mon Sep 17 00:00:00 2001 From: Ankita Date: Tue, 30 Jul 2019 10:05:39 -0700 Subject: [PATCH 38/74] Adding http client to call fis backend service (#659) * Adding Firebase Installations module * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Http client to call FIS backend service. * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. * Http client to call FIS backend service. * Addresing comments and introducing new FirebaseInstallationService Exception. --- .../InstallationTokenResult.java | 22 +- .../firebase-installations.gradle | 4 + .../installations/FirebaseInstallations.java | 2 +- .../FirebaseInstallationServiceClient.java | 256 ++++++++++++++++++ .../FirebaseInstallationServiceException.java | 57 ++++ .../remote/InstallationResponse.java | 56 ++++ 6 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index df04f26ca38..5e2450b14d6 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -28,11 +28,27 @@ public abstract class InstallationTokenResult { * The amount of time, in milliseconds, before the auth-token expires for this firebase * installation. */ + @NonNull public abstract long getTokenExpirationTimestampMillis(); @NonNull - public static InstallationTokenResult create( - @NonNull String authToken, long tokenExpirationTimestampMillis) { - return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationTokenResult.Builder builder() { + return new AutoValue_InstallationTokenResult.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setAuthToken(@NonNull String value); + + @NonNull + public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); + + @NonNull + public abstract InstallationTokenResult build(); } } diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 45123f1c6ce..ec0224515dd 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -45,6 +45,10 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + 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" diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index f135aa2e197..e33664b6e32 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -78,7 +78,7 @@ public Task getId() { @NonNull @Override public Task getAuthToken(boolean forceRefresh) { - return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + return Tasks.forResult(InstallationTokenResult.builder().build()); } /** diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java new file mode 100644 index 00000000000..51cf2e1a219 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -0,0 +1,256 @@ +// 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.installations.remote; + +import android.util.JsonReader; +import androidx.annotation.NonNull; +import com.google.firebase.installations.InstallationTokenResult; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.json.JSONException; +import org.json.JSONObject; + +/** Http client that sends request to Firebase Installations backend API. */ +public class FirebaseInstallationServiceClient { + private static final String FIREBASE_INSTALLATIONS_API_DOMAIN = + "firebaseinstallations.googleapis.com"; + private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations"; + private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/auth:generate"; + private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s"; + private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1"; + private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_V2"; + + private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; + private static final String ACCEPT_HEADER_KEY = "Accept"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; + private static final String GZIP_CONTENT_ENCODING = "gzip"; + + private static final String UNAUTHORIZED_ERROR_MESSAGE = + "The request did not have the required credentials."; + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; + private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + + @NonNull + public InstallationResponse createFirebaseInstallation( + long projectNumber, + @NonNull String apiKey, + @NonNull String firebaseInstallationId, + @NonNull String appId) + throws FirebaseInstallationServiceException { + String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(firebaseInstallationId, appId) + .toString() + .getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readCreateResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, String appId) + throws JSONException { + JSONObject firebaseInstallationData = new JSONObject(); + firebaseInstallationData.put("fid", fid); + firebaseInstallationData.put("appId", appId); + firebaseInstallationData.put("appVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + return firebaseInstallationData; + } + + @NonNull + public void deleteFirebaseInstallation( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("DELETE"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return; + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + @NonNull + public InstallationTokenResult generateAuthToken( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = + String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readGenerateAuthTokenResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + // Read the response from the createFirebaseInstallation API. + private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); + InstallationResponse.Builder builder = InstallationResponse.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("name")) { + builder.setName(reader.nextString()); + } else if (name.equals("refreshToken")) { + builder.setRefreshToken(reader.nextString()); + } else if (name.equals("authToken")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (key.equals("token")) { + installationTokenResult.setAuthToken(reader.nextString()); + } else if (key.equals("expiresIn")) { + installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + builder.setAuthToken(installationTokenResult.build()); + reader.endObject(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } + + // Read the response from the generateAuthToken FirebaseInstallation API. + private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) + throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("token")) { + builder.setAuthToken(reader.nextString()); + } else if (name.equals("expiresIn")) { + builder.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java new file mode 100644 index 00000000000..b703b56d539 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java @@ -0,0 +1,57 @@ +// 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.installations.remote; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ +public class FirebaseInstallationServiceException extends FirebaseException { + + public enum Code { + SERVER_ERROR, + + NETWORK_ERROR, + + UNAUTHORIZED + } + + @NonNull private final Code code; + + FirebaseInstallationServiceException(@NonNull Code code) { + this.code = code; + } + + FirebaseInstallationServiceException(@NonNull String message, @NonNull Code code) { + super(message); + this.code = code; + } + + FirebaseInstallationServiceException( + @NonNull String message, @NonNull Code code, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * Gets the status code for the operation that failed. + * + * @return the code for the FirebaseInstallationServiceException + */ + @NonNull + public Code getCode() { + return code; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java new file mode 100644 index 00000000000..2022a498fe1 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations.remote; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; +import com.google.firebase.installations.InstallationTokenResult; + +@AutoValue +public abstract class InstallationResponse { + + @NonNull + public abstract String getName(); + + @NonNull + public abstract String getRefreshToken(); + + @NonNull + public abstract InstallationTokenResult getAuthToken(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationResponse.Builder builder() { + return new AutoValue_InstallationResponse.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setName(@NonNull String value); + + @NonNull + public abstract Builder setRefreshToken(@NonNull String value); + + @NonNull + public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + + @NonNull + public abstract InstallationResponse build(); + } +} From 143ed7451b48dbbf8ca510017bca201546f30bde Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 30 Jul 2019 11:10:48 -0700 Subject: [PATCH 39/74] FirebaseSegmentation SDK 1. Clean up http client response code. 2. When updateCustomInstallationId is called, on non-retryable server errors, the SDK should clean up the local cache. Instead, for retryable errors, SDK can keep the local cache for retrying update later. --- .../FirebaseSegmentationInstrumentedTest.java | 28 +++++++++++++- .../segmentation/FirebaseSegmentation.java | 37 ++++++++++++++----- .../SetCustomInstallationIdException.java | 4 +- .../remote/SegmentationServiceClient.java | 14 +++++-- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 19498782a78..034ee1eb339 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 @@ -127,7 +127,8 @@ public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception } @Test - public void testUpdateCustomInstallationId_CacheOk_BackendError() throws InterruptedException { + public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() + throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); @@ -150,6 +151,31 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError() throws Interru .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() + throws InterruptedException { + when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(SegmentationServiceClient.Code.CONFLICT); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue).isNull(); + } + @Test public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 2008ea09359..85adf2fb708 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -128,7 +128,7 @@ private Void updateCustomInstallationId(String customInstallationId) instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to get Firebase instance id"); } boolean firstUpdateCacheResult = @@ -140,7 +140,7 @@ private Void updateCustomInstallationId(String customInstallationId) if (!firstUpdateCacheResult) { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } // Start requesting backend when first cache updae is done. @@ -164,11 +164,22 @@ private Void updateCustomInstallationId(String customInstallationId) instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.SYNCED)); break; - case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + case UNAUTHORIZED: + localCache.clear(); + throw new SetCustomInstallationIdException( + Status.CLIENT_ERROR, "Instance id token is invalid."); case CONFLICT: - throw new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + localCache.clear(); + throw new SetCustomInstallationIdException( + Status.DUPLICATED_CUSTOM_INSTALLATION_ID, + "The custom installation id is used by another Firebase installation in your project."); + case HTTP_CLIENT_ERROR: + localCache.clear(); + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); + case NETWORK_ERROR: + case SERVER_ERROR: default: + // These are considered retryable errors, so not to clean up the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } @@ -176,7 +187,7 @@ private Void updateCustomInstallationId(String customInstallationId) return null; } else { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } } @@ -204,7 +215,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to get Firebase instance id"); } boolean firstUpdateCacheResult = @@ -214,7 +225,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException if (!firstUpdateCacheResult) { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } String iid = instanceIdResult.getId(); @@ -231,9 +242,15 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException case OK: finalUpdateCacheResult = localCache.clear(); break; + case UNAUTHORIZED: + throw new SetCustomInstallationIdException( + Status.CLIENT_ERROR, "Instance id token is invalid."); case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); + case NETWORK_ERROR: + case SERVER_ERROR: default: + // These are considered retryable errors, so not to clean up the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } @@ -241,7 +258,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException return null; } else { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java index 3c957ce3294..2291c078a03 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java @@ -45,13 +45,13 @@ public enum Status { this.status = status; } - SetCustomInstallationIdException(@NonNull String message, @NonNull Status status) { + SetCustomInstallationIdException(@NonNull Status status, @NonNull String message) { super(message); this.status = status; } SetCustomInstallationIdException( - @NonNull String message, @NonNull Status status, Throwable cause) { + @NonNull Status status, @NonNull String message, Throwable cause) { super(message, cause); this.status = status; } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 86b5f945205..6d0436d8b24 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -41,15 +41,15 @@ public class SegmentationServiceClient { public enum Code { OK, - HTTP_CLIENT_ERROR, - CONFLICT, + UNAUTHORIZED, + NETWORK_ERROR, - SERVER_ERROR, + HTTP_CLIENT_ERROR, - UNAUTHORIZED, + SERVER_ERROR, } @NonNull @@ -100,6 +100,9 @@ public Code updateCustomInstallationId( case 409: return Code.CONFLICT; default: + if (httpResponseCode / 100 == 4) { + return Code.HTTP_CLIENT_ERROR; + } return Code.SERVER_ERROR; } } catch (IOException e) { @@ -158,6 +161,9 @@ public Code clearCustomInstallationId( case 401: return Code.UNAUTHORIZED; default: + if (httpResponseCode / 100 == 4) { + return Code.HTTP_CLIENT_ERROR; + } return Code.SERVER_ERROR; } } catch (IOException e) { From 52d259db3804bed609b5354c7502eddc68133dad Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Tue, 6 Aug 2019 11:16:50 -0700 Subject: [PATCH 40/74] FirebaseSegmentation SDK changes (#673) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit * Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) * minor format fix * Address comments #1 * Http client in Firebase Segmentation SDK to call backend service. * Revert unintentional change * Fix connected device test * Fix connected device test * 1. Add a few annotations to make java code Kotlin friendly 2. Some fixes for the http request format * Fix java format * Fix API version * Change the segmentation API implementation to synchronous and put the entire synchronous code block in async task. * Fix a async getResult race issue. * OkHttpClient -> HttpsUrlConnection * Use gzip for compressing content and fix ourput stream memory leak risk. * Addressed a few comments * FirebaseSegmentation SDK 1. Clean up http client response code. 2. When updateCustomInstallationId is called, on non-retryable server errors, the SDK should clean up the local cache. Instead, for retryable errors, SDK can keep the local cache for retrying update later. --- .../FirebaseSegmentationInstrumentedTest.java | 28 +++++++++++++- .../segmentation/FirebaseSegmentation.java | 37 ++++++++++++++----- .../SetCustomInstallationIdException.java | 4 +- .../remote/SegmentationServiceClient.java | 14 +++++-- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java index 19498782a78..034ee1eb339 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 @@ -127,7 +127,8 @@ public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception } @Test - public void testUpdateCustomInstallationId_CacheOk_BackendError() throws InterruptedException { + public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() + throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); @@ -150,6 +151,31 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError() throws Interru .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } + @Test + public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() + throws InterruptedException { + when(backendClientReturnsError.updateCustomInstallationId( + anyLong(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(SegmentationServiceClient.Code.CONFLICT); + FirebaseSegmentation firebaseSegmentation = + new FirebaseSegmentation( + firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + + // Expect exception + try { + Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); + assertThat(((SetCustomInstallationIdException) cause).getStatus()) + .isEqualTo(SetCustomInstallationIdException.Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + } + + CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); + assertThat(entryValue).isNull(); + } + @Test public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 2008ea09359..85adf2fb708 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -128,7 +128,7 @@ private Void updateCustomInstallationId(String customInstallationId) instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to get Firebase instance id"); } boolean firstUpdateCacheResult = @@ -140,7 +140,7 @@ private Void updateCustomInstallationId(String customInstallationId) if (!firstUpdateCacheResult) { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } // Start requesting backend when first cache updae is done. @@ -164,11 +164,22 @@ private Void updateCustomInstallationId(String customInstallationId) instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.SYNCED)); break; - case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + case UNAUTHORIZED: + localCache.clear(); + throw new SetCustomInstallationIdException( + Status.CLIENT_ERROR, "Instance id token is invalid."); case CONFLICT: - throw new SetCustomInstallationIdException(Status.DUPLICATED_CUSTOM_INSTALLATION_ID); + localCache.clear(); + throw new SetCustomInstallationIdException( + Status.DUPLICATED_CUSTOM_INSTALLATION_ID, + "The custom installation id is used by another Firebase installation in your project."); + case HTTP_CLIENT_ERROR: + localCache.clear(); + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); + case NETWORK_ERROR: + case SERVER_ERROR: default: + // These are considered retryable errors, so not to clean up the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } @@ -176,7 +187,7 @@ private Void updateCustomInstallationId(String customInstallationId) return null; } else { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } } @@ -204,7 +215,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - "Failed to get Firebase instance id", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to get Firebase instance id"); } boolean firstUpdateCacheResult = @@ -214,7 +225,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException if (!firstUpdateCacheResult) { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } String iid = instanceIdResult.getId(); @@ -231,9 +242,15 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException case OK: finalUpdateCacheResult = localCache.clear(); break; + case UNAUTHORIZED: + throw new SetCustomInstallationIdException( + Status.CLIENT_ERROR, "Instance id token is invalid."); case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR); + throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); + case NETWORK_ERROR: + case SERVER_ERROR: default: + // These are considered retryable errors, so not to clean up the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } @@ -241,7 +258,7 @@ private Void clearCustomInstallationId() throws SetCustomInstallationIdException return null; } else { throw new SetCustomInstallationIdException( - "Failed to update client side cache", Status.CLIENT_ERROR); + Status.CLIENT_ERROR, "Failed to update client side cache"); } } } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java index 3c957ce3294..2291c078a03 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java @@ -45,13 +45,13 @@ public enum Status { this.status = status; } - SetCustomInstallationIdException(@NonNull String message, @NonNull Status status) { + SetCustomInstallationIdException(@NonNull Status status, @NonNull String message) { super(message); this.status = status; } SetCustomInstallationIdException( - @NonNull String message, @NonNull Status status, Throwable cause) { + @NonNull Status status, @NonNull String message, Throwable cause) { super(message, cause); this.status = status; } diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 86b5f945205..6d0436d8b24 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -41,15 +41,15 @@ public class SegmentationServiceClient { public enum Code { OK, - HTTP_CLIENT_ERROR, - CONFLICT, + UNAUTHORIZED, + NETWORK_ERROR, - SERVER_ERROR, + HTTP_CLIENT_ERROR, - UNAUTHORIZED, + SERVER_ERROR, } @NonNull @@ -100,6 +100,9 @@ public Code updateCustomInstallationId( case 409: return Code.CONFLICT; default: + if (httpResponseCode / 100 == 4) { + return Code.HTTP_CLIENT_ERROR; + } return Code.SERVER_ERROR; } } catch (IOException e) { @@ -158,6 +161,9 @@ public Code clearCustomInstallationId( case 401: return Code.UNAUTHORIZED; default: + if (httpResponseCode / 100 == 4) { + return Code.HTTP_CLIENT_ERROR; + } return Code.SERVER_ERROR; } } catch (IOException e) { From d467379ab17f48719cfbafe4eafbbf6d86af8fc2 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 6 Aug 2019 11:22:26 -0700 Subject: [PATCH 41/74] Restrict Firebase API key to Android app package name. --- .../segmentation/FirebaseSegmentation.java | 4 +- .../remote/SegmentationServiceClient.java | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 85adf2fb708..b1d5e6e6742 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -35,6 +35,8 @@ /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { + public static final String TAG = "FirebaseSegmentation"; + private final FirebaseApp firebaseApp; private final FirebaseInstanceId firebaseInstanceId; private final CustomInstallationIdCache localCache; @@ -46,7 +48,7 @@ public class FirebaseSegmentation { firebaseApp, FirebaseInstanceId.getInstance(firebaseApp), new CustomInstallationIdCache(firebaseApp), - new SegmentationServiceClient()); + new SegmentationServiceClient(firebaseApp.getApplicationContext())); } FirebaseSegmentation( diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 6d0436d8b24..65058bb97da 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -14,7 +14,14 @@ package com.google.firebase.segmentation.remote; +import static com.google.firebase.segmentation.FirebaseSegmentation.TAG; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Log; import androidx.annotation.NonNull; +import com.google.android.gms.common.util.AndroidUtilsLight; +import com.google.android.gms.common.util.Hex; import java.io.IOException; import java.net.URL; import java.util.zip.GZIPOutputStream; @@ -37,6 +44,14 @@ public class SegmentationServiceClient { private static final String JSON_CONTENT_TYPE = "application/json"; private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; private static final String GZIP_CONTENT_ENCODING = "gzip"; + private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; + private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; + + private final Context context; + + public SegmentationServiceClient(@NonNull Context context) { + this.context = context; + } public enum Code { OK, @@ -78,6 +93,9 @@ public Code updateCustomInstallationId( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { @@ -143,6 +161,9 @@ public Code clearCustomInstallationId( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { @@ -175,4 +196,24 @@ private static JSONObject buildClearCustomSegmentationDataRequestBody(String res throws JSONException { return new JSONObject().put("name", resourceName); } + + /** Gets the Android package's SHA-1 fingerprint. */ + private String getFingerprintHashForPackage() { + byte[] hash; + + try { + hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName()); + + if (hash == null) { + Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName()); + return null; + } else { + String cert = Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); + return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "No such package: " + context.getPackageName(), e); + return null; + } + } } From 33665b2d47d00614e025d20860dcae68a7ce1b2f Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Tue, 6 Aug 2019 11:57:00 -0700 Subject: [PATCH 42/74] Restrict Firebase API key to Android app package name. (#690) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit * Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) * minor format fix * Address comments #1 * Http client in Firebase Segmentation SDK to call backend service. * Revert unintentional change * Fix connected device test * Fix connected device test * 1. Add a few annotations to make java code Kotlin friendly 2. Some fixes for the http request format * Fix java format * Fix API version * Change the segmentation API implementation to synchronous and put the entire synchronous code block in async task. * Fix a async getResult race issue. * OkHttpClient -> HttpsUrlConnection * Use gzip for compressing content and fix ourput stream memory leak risk. * Addressed a few comments * FirebaseSegmentation SDK 1. Clean up http client response code. 2. When updateCustomInstallationId is called, on non-retryable server errors, the SDK should clean up the local cache. Instead, for retryable errors, SDK can keep the local cache for retrying update later. * Restrict Firebase API key to Android app package name. --- .../segmentation/FirebaseSegmentation.java | 4 +- .../remote/SegmentationServiceClient.java | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java index 85adf2fb708..b1d5e6e6742 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java @@ -35,6 +35,8 @@ /** Entry point of Firebase Segmentation SDK. */ public class FirebaseSegmentation { + public static final String TAG = "FirebaseSegmentation"; + private final FirebaseApp firebaseApp; private final FirebaseInstanceId firebaseInstanceId; private final CustomInstallationIdCache localCache; @@ -46,7 +48,7 @@ public class FirebaseSegmentation { firebaseApp, FirebaseInstanceId.getInstance(firebaseApp), new CustomInstallationIdCache(firebaseApp), - new SegmentationServiceClient()); + new SegmentationServiceClient(firebaseApp.getApplicationContext())); } FirebaseSegmentation( diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java index 6d0436d8b24..65058bb97da 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java @@ -14,7 +14,14 @@ package com.google.firebase.segmentation.remote; +import static com.google.firebase.segmentation.FirebaseSegmentation.TAG; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Log; import androidx.annotation.NonNull; +import com.google.android.gms.common.util.AndroidUtilsLight; +import com.google.android.gms.common.util.Hex; import java.io.IOException; import java.net.URL; import java.util.zip.GZIPOutputStream; @@ -37,6 +44,14 @@ public class SegmentationServiceClient { private static final String JSON_CONTENT_TYPE = "application/json"; private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; private static final String GZIP_CONTENT_ENCODING = "gzip"; + private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; + private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; + + private final Context context; + + public SegmentationServiceClient(@NonNull Context context) { + this.context = context; + } public enum Code { OK, @@ -78,6 +93,9 @@ public Code updateCustomInstallationId( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { @@ -143,6 +161,9 @@ public Code clearCustomInstallationId( "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { @@ -175,4 +196,24 @@ private static JSONObject buildClearCustomSegmentationDataRequestBody(String res throws JSONException { return new JSONObject().put("name", resourceName); } + + /** Gets the Android package's SHA-1 fingerprint. */ + private String getFingerprintHashForPackage() { + byte[] hash; + + try { + hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName()); + + if (hash == null) { + Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName()); + return null; + } else { + String cert = Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); + return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "No such package: " + context.getPackageName(), e); + return null; + } + } } From 818e4717b0192ea1f68dc77966d5082e0d1aae0a Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 6 Aug 2019 12:54:45 -0700 Subject: [PATCH 43/74] Explicitly add internet permission --- firebase-segmentation/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-segmentation/src/main/AndroidManifest.xml b/firebase-segmentation/src/main/AndroidManifest.xml index 1502b31485f..9005c369f32 100644 --- a/firebase-segmentation/src/main/AndroidManifest.xml +++ b/firebase-segmentation/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ + Date: Tue, 6 Aug 2019 13:43:47 -0700 Subject: [PATCH 44/74] Arete floc (#691) * Implement Firebase segmentation SDK device local cache * [Firebase Segmentation] Add custom installation id cache layer and tests for it. * Add test for updating cache * Switch to use SQLiteOpenHelper * Switch to use SharedPreferences from SQLite. * Change the cache class to be singleton * Wrap shared pref commit in a async task. * Address comments * Google format fix * Replace some deprecated code. * Package refactor * nit * nit * Add the state machine of updating custom installation id in the local cache and update to Firebase Segmentation backend. CL also contains unit tests. (The http client is not implemented yet.) * minor format fix * Address comments #1 * Http client in Firebase Segmentation SDK to call backend service. * Revert unintentional change * Fix connected device test * Fix connected device test * 1. Add a few annotations to make java code Kotlin friendly 2. Some fixes for the http request format * Fix java format * Fix API version * Change the segmentation API implementation to synchronous and put the entire synchronous code block in async task. * Fix a async getResult race issue. * OkHttpClient -> HttpsUrlConnection * Use gzip for compressing content and fix ourput stream memory leak risk. * Addressed a few comments * FirebaseSegmentation SDK 1. Clean up http client response code. 2. When updateCustomInstallationId is called, on non-retryable server errors, the SDK should clean up the local cache. Instead, for retryable errors, SDK can keep the local cache for retrying update later. * Restrict Firebase API key to Android app package name. * Explicitly add internet permission --- firebase-segmentation/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-segmentation/src/main/AndroidManifest.xml b/firebase-segmentation/src/main/AndroidManifest.xml index 1502b31485f..9005c369f32 100644 --- a/firebase-segmentation/src/main/AndroidManifest.xml +++ b/firebase-segmentation/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ + Date: Wed, 7 Aug 2019 14:05:07 -0700 Subject: [PATCH 45/74] Implementing cache for FIS SDK (#694) * Implementing cache for FIS SDK * Implementing cache for FIS SDK * Addressing Di's comments. * Addressing Di's comments. --- .../firebase-installations.gradle | 5 +- .../src/androidTest/AndroidManifest.xml | 26 ++++ .../installation/local/FiidCacheTest.java | 110 +++++++++++++++++ .../installations/FirebaseInstallations.java | 2 +- .../installations/local/FiidCache.java | 113 ++++++++++++++++++ .../local/FiidCacheEntryValue.java | 59 +++++++++ 6 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 firebase-installations/src/androidTest/AndroidManifest.xml create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index ec0224515dd..c5fea1c252b 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -53,6 +53,9 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' - implementation 'com.google.guava:guava:16.0.+' + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.12' + androidTestImplementation "androidx.annotation:annotation:1.0.0" } diff --git a/firebase-installations/src/androidTest/AndroidManifest.xml b/firebase-installations/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000000..43c54f991fd --- /dev/null +++ b/firebase-installations/src/androidTest/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java new file mode 100644 index 00000000000..c43a6729610 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java @@ -0,0 +1,110 @@ +// 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.installation.local; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.FiidCache; +import com.google.firebase.installations.local.FiidCacheEntryValue; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumented tests for {@link FiidCache} */ +@RunWith(AndroidJUnit4.class) +public class FiidCacheTest { + + private FirebaseApp firebaseApp0; + private FirebaseApp firebaseApp1; + private FiidCache cache0; + private FiidCache cache1; + private final String AUTH_TOKEN = "auth_token"; + private final String REFRESH_TOKEN = "refresh_token"; + + private final long TIMESTAMP_IN_SECONDS = 100L; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp0 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + firebaseApp1 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + cache0 = new FiidCache(firebaseApp0); + cache1 = new FiidCache(firebaseApp1); + } + + @After + public void cleanUp() throws Exception { + cache0.clear(); + cache1.clear(); + } + + @Test + public void testReadCacheEntry_Null() { + assertNull(cache0.readCacheEntryValue()); + assertNull(cache1.readCacheEntryValue()); + } + + @Test + public void testUpdateAndReadCacheEntry() throws Exception { + assertTrue( + cache0.insertOrUpdateCacheEntry( + FiidCacheEntryValue.create( + "123456", + FiidCache.CacheStatus.UNREGISTERED, + AUTH_TOKEN, + REFRESH_TOKEN, + TIMESTAMP_IN_SECONDS, + TIMESTAMP_IN_SECONDS))); + FiidCacheEntryValue entryValue = cache0.readCacheEntryValue(); + assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); + assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); + assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.UNREGISTERED); + assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertNull(cache1.readCacheEntryValue()); + + assertTrue( + cache0.insertOrUpdateCacheEntry( + FiidCacheEntryValue.create( + "123456", + FiidCache.CacheStatus.REGISTERED, + AUTH_TOKEN, + REFRESH_TOKEN, + 200L, + TIMESTAMP_IN_SECONDS))); + entryValue = cache0.readCacheEntryValue(); + assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); + assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); + assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTERED); + assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(200L); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index e33664b6e32..75affe7e15f 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -15,10 +15,10 @@ package com.google.firebase.installations; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; -import com.google.common.annotations.VisibleForTesting; import com.google.firebase.FirebaseApp; /** diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java new file mode 100644 index 00000000000..589c9fb9266 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java @@ -0,0 +1,113 @@ +// 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.installations.local; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.FirebaseApp; + +/** + * A layer that locally caches a few Firebase Installation attributes on top the Firebase + * Installation backend API. + */ +public class FiidCache { + // 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. + public enum CacheStatus { + // Cache entry is synced to Firebase backend + REGISTERED, + // Cache entry is waiting for Firebase backend response or internal network retry + UNREGISTERED, + // Cache entry is in error state when syncing with Firebase backend + REGISTER_ERROR, + // Cache entry is in delete state before syncing with Firebase backend + DELETED + } + + private static final String SHARED_PREFS_NAME = "FiidCache"; + + private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; + private static final String AUTH_TOKEN_KEY = "AuthToken"; + private static final String REFRESH_TOKEN_KEY = "RefreshToken"; + private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; + private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; + private static final String CACHE_STATUS_KEY = "Status"; + + private final SharedPreferences prefs; + private final String persistenceKey; + + public FiidCache(@NonNull FirebaseApp firebaseApp) { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path + prefs = + firebaseApp + .getApplicationContext() + .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + persistenceKey = firebaseApp.getPersistenceKey(); + } + + @Nullable + public synchronized FiidCacheEntryValue readCacheEntryValue() { + String iid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); + String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); + String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); + long tokenCreationTime = + prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); + long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + + if (iid == null || status == -1) { + return null; + } + + return FiidCacheEntryValue.create( + iid, CacheStatus.values()[status], authToken, refreshToken, tokenCreationTime, expiresIn); + } + + @NonNull + public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValue entryValue) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString( + getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), + entryValue.getFirebaseInstallationId()); + editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); + editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); + editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); + editor.putLong( + getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), + entryValue.getTokenCreationEpochInSecs()); + editor.putLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); + return editor.commit(); + } + + @NonNull + public synchronized boolean clear() { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY)); + editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); + editor.remove(getSharedPreferencesKey(AUTH_TOKEN_KEY)); + editor.remove(getSharedPreferencesKey(REFRESH_TOKEN_KEY)); + editor.remove(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY)); + editor.remove(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY)); + return editor.commit(); + } + + private String getSharedPreferencesKey(String key) { + return String.format("%s|%s", persistenceKey, key); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java new file mode 100644 index 00000000000..3d42ca15bbf --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java @@ -0,0 +1,59 @@ +// 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.installations.local; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** + * This class represents a cache entry value in {@link FiidCache}, which contains a Firebase + * instance id and the cache status of this entry. + */ +@AutoValue +public abstract class FiidCacheEntryValue { + + @NonNull + public abstract String getFirebaseInstallationId(); + + @NonNull + public abstract FiidCache.CacheStatus getCacheStatus(); + + @NonNull + public abstract String getAuthToken(); + + @NonNull + public abstract String getRefreshToken(); + + public abstract long getExpiresInSecs(); + + public abstract long getTokenCreationEpochInSecs(); + + @NonNull + public static FiidCacheEntryValue create( + @NonNull String firebaseInstallationId, + @NonNull FiidCache.CacheStatus cacheStatus, + @NonNull String authToken, + @NonNull String refreshToken, + long tokenCreationEpochInSecs, + long expiresInSecs) { + return new AutoValue_FiidCacheEntryValue( + firebaseInstallationId, + cacheStatus, + authToken, + refreshToken, + expiresInSecs, + tokenCreationEpochInSecs); + } +} From 77b6d973b6988b524d0d4cdd47e43fb04425e21f Mon Sep 17 00:00:00 2001 From: Ankita Date: Fri, 30 Aug 2019 14:06:10 -0700 Subject: [PATCH 46/74] Adding Util class for FIrebaseInstallations APIs. (#676) * Adding Firebase Installations module * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Http client to call FIS backend service. * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. * Http client to call FIS backend service. * Addresing comments and introducing new FirebaseInstallationService Exception. * Implementing getId method in FirebaseInstallation to call backend. * 1. Adding instrumentation tests. 2. Introducing serviceClient level and main class exceptions. * Addressing Di's comments. * Addressing Rayo's comments * Updating parameters order in ServiceClient. * Updating createRandomFid as per Rayo's comments. * getId() implementation with instrumented tests. (#703) * getId() implementation with instrumented tests. * Addressing rayo's comments. - Detailed JavaDocs - Renaming FiidCache as PersistedFid * Addressing rayo's comments. - Detailed JavaDocs - Renaming FiidCache as PersistedFid * Addresing comments to resoleve the following: - Make registerAnsSaveFid non blocking call in getId() - PersistedFidEntry builder with default values * Addressing Ciaran and Rayo's comments. * Addressing Ciaran's comments * Addressing Ciaran's comments * Adding param comments and checking if registration status is valid. * Correcting lint warning: uses-permission should be declared before application in AndroidManifest.xml * Adding custom assertThat with more readable assertions * Correcting instrumented tests to be reliable. --- .../InstallationTokenResult.java | 10 +- .../firebase-installations.gradle | 2 + .../src/androidTest/AndroidManifest.xml | 2 +- .../installation/local/FiidCacheTest.java | 110 --------- ...FirebaseInstallationsInstrumentedTest.java | 220 ++++++++++++++++++ .../FisAndroidTestConstants.java | 33 +++ .../local/PersistedFidEntrySubject.java | 85 +++++++ .../installations/local/PersistedFidTest.java | 120 ++++++++++ .../src/main/AndroidManifest.xml | 3 +- .../installations/FirebaseInstallations.java | 147 +++++++++++- .../FirebaseInstallationsException.java | 56 +++++ .../google/firebase/installations/Utils.java | 86 +++++++ .../local/FiidCacheEntryValue.java | 59 ----- .../{FiidCache.java => PersistedFid.java} | 78 ++++--- .../local/PersistedFidEntry.java | 78 +++++++ .../FirebaseInstallationServiceClient.java | 83 ++++--- .../FirebaseInstallationServiceException.java | 27 ++- .../FirebaseInstallationsRegistrarTest.java | 39 +++- 18 files changed, 989 insertions(+), 249 deletions(-) delete mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/Utils.java delete mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java rename firebase-installations/src/main/java/com/google/firebase/installations/local/{FiidCache.java => PersistedFid.java} (58%) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index 5e2450b14d6..5cd761299eb 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -21,12 +21,12 @@ @AutoValue public abstract class InstallationTokenResult { - /** A new FIS Auth-Token, created for this firebase installation. */ + /** A new FIS Auth-Token, created for this Firebase Installation. */ @NonNull - public abstract String getAuthToken(); + public abstract String getToken(); /** - * The amount of time, in milliseconds, before the auth-token expires for this firebase - * installation. + * The amount of time, in milliseconds, before the auth-token expires for this Firebase + * Installation. */ @NonNull public abstract long getTokenExpirationTimestampMillis(); @@ -43,7 +43,7 @@ public static InstallationTokenResult.Builder builder() { @AutoValue.Builder public abstract static class Builder { @NonNull - public abstract Builder setAuthToken(@NonNull String value); + public abstract Builder setToken(@NonNull String value); @NonNull public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index c5fea1c252b..39b289c0834 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -58,4 +58,6 @@ dependencies { androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'junit:junit:4.12' androidTestImplementation "androidx.annotation:annotation:1.0.0" + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' } diff --git a/firebase-installations/src/androidTest/AndroidManifest.xml b/firebase-installations/src/androidTest/AndroidManifest.xml index 43c54f991fd..f9fb55a7b56 100644 --- a/firebase-installations/src/androidTest/AndroidManifest.xml +++ b/firebase-installations/src/androidTest/AndroidManifest.xml @@ -14,7 +14,7 @@ + package="com.google.firebase.installations"> diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java deleted file mode 100644 index c43a6729610..00000000000 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java +++ /dev/null @@ -1,110 +0,0 @@ -// 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.installation.local; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.runner.AndroidJUnit4; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.local.FiidCache; -import com.google.firebase.installations.local.FiidCacheEntryValue; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Instrumented tests for {@link FiidCache} */ -@RunWith(AndroidJUnit4.class) -public class FiidCacheTest { - - private FirebaseApp firebaseApp0; - private FirebaseApp firebaseApp1; - private FiidCache cache0; - private FiidCache cache1; - private final String AUTH_TOKEN = "auth_token"; - private final String REFRESH_TOKEN = "refresh_token"; - - private final long TIMESTAMP_IN_SECONDS = 100L; - - @Before - public void setUp() { - FirebaseApp.clearInstancesForTest(); - firebaseApp0 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); - firebaseApp1 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), - "firebase_app_1"); - cache0 = new FiidCache(firebaseApp0); - cache1 = new FiidCache(firebaseApp1); - } - - @After - public void cleanUp() throws Exception { - cache0.clear(); - cache1.clear(); - } - - @Test - public void testReadCacheEntry_Null() { - assertNull(cache0.readCacheEntryValue()); - assertNull(cache1.readCacheEntryValue()); - } - - @Test - public void testUpdateAndReadCacheEntry() throws Exception { - assertTrue( - cache0.insertOrUpdateCacheEntry( - FiidCacheEntryValue.create( - "123456", - FiidCache.CacheStatus.UNREGISTERED, - AUTH_TOKEN, - REFRESH_TOKEN, - TIMESTAMP_IN_SECONDS, - TIMESTAMP_IN_SECONDS))); - FiidCacheEntryValue entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); - assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); - assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.UNREGISTERED); - assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertNull(cache1.readCacheEntryValue()); - - assertTrue( - cache0.insertOrUpdateCacheEntry( - FiidCacheEntryValue.create( - "123456", - FiidCache.CacheStatus.REGISTERED, - AUTH_TOKEN, - REFRESH_TOKEN, - 200L, - TIMESTAMP_IN_SECONDS))); - entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); - assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); - assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTERED); - assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(200L); - } -} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java new file mode 100644 index 00000000000..781747f5a5e --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -0,0 +1,220 @@ +// 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.installations; + +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.PersistedFid; +import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.FirebaseInstallationServiceException; +import com.google.firebase.installations.remote.InstallationResponse; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class FirebaseInstallationsInstrumentedTest { + private FirebaseApp firebaseApp; + private ExecutorService executor; + private PersistedFid persistedFid; + @Mock private FirebaseInstallationServiceClient backendClientReturnsOk; + @Mock private FirebaseInstallationServiceClient backendClientReturnsError; + @Mock private PersistedFid persistedFidReturnsError; + @Mock private Utils mockUtils; + @Mock private Clock mockClock; + + @Before + public void setUp() throws FirebaseInstallationServiceException { + MockitoAnnotations.initMocks(this); + FirebaseApp.clearInstancesForTest(); + executor = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new SynchronousQueue<>()); + firebaseApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder() + .setApplicationId(TEST_APP_ID_1) + .setProjectId(TEST_PROJECT_ID) + .setApiKey("api_key") + .build()); + persistedFid = new PersistedFid(firebaseApp); + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder() + .setName("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestampMillis(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .build()); + when(backendClientReturnsError.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenThrow( + new FirebaseInstallationServiceException( + "SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + when(persistedFidReturnsError.insertOrUpdatePersistedFidEntry(any())).thenReturn(false); + when(persistedFidReturnsError.readPersistedFidEntryValue()).thenReturn(null); + when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); + when(mockClock.currentTimeMillis()).thenReturn(TEST_CREATION_TIMESTAMP_1); + } + + @After + public void cleanUp() throws Exception { + persistedFid.clear(); + } + + @Test + public void testGetId_PersistedFidOk_BackendOk() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + // No exception, means success. + assertWithMessage("getId Task fails.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration status doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + } + + @Test + public void testGetId_multipleCalls_sameFIDReturned() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + // No exception, means success. + assertWithMessage("getId Task fails.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + Tasks.await(firebaseInstallations.getId()); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration status doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + } + + @Test + public void testGetId_PersistedFidOk_BackendError() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.getId()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration Fid doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTER_ERROR); + } + + @Test + public void testGetId_PersistedFidError_BackendOk() throws InterruptedException { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, + executor, + firebaseApp, + backendClientReturnsOk, + persistedFidReturnsError, + mockUtils); + + // Expect exception + try { + Tasks.await(firebaseInstallations.getId()); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertWithMessage("Exception class doesn't match") + .that(cause) + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) cause).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); + } + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java new file mode 100644 index 00000000000..97f3989f1de --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +public final class FisAndroidTestConstants { + public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + + public static final String TEST_PROJECT_ID = "777777777777"; + + public static final String TEST_AUTH_TOKEN = "fis.auth.token"; + + public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; + + public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; + public static final String TEST_APP_ID_2 = "1:987654321:android:abcdef"; + + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 1000L; + + public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; + public static final long TEST_CREATION_TIMESTAMP_2 = 2000L; +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java new file mode 100644 index 00000000000..48586508823 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java @@ -0,0 +1,85 @@ +// 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.installations.local; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +public final class PersistedFidEntrySubject extends Subject { + + // User-defined entry point + public static PersistedFidEntrySubject assertThat( + @NullableDecl PersistedFidEntry persistedFidEntry) { + return assertAbout(PERSISTED_FID_ENTRY_SUBJECT_FACTORY).that(persistedFidEntry); + } + + // Static method for getting the subject factory (for use with assertAbout()) + public static Subject.Factory persistedFidEntry() { + return PERSISTED_FID_ENTRY_SUBJECT_FACTORY; + } + + // Boiler-plate Subject.Factory for EmployeeSubject + private static final Subject.Factory + PERSISTED_FID_ENTRY_SUBJECT_FACTORY = PersistedFidEntrySubject::new; + + private final PersistedFidEntry actual; + + /** + * Constructor for use by subclasses. If you want to create an instance of this class itself, call + * {@link Subject#check(String, PersistedFidEntry ..) check(...)}{@code .that(actual)}. + * + * @param metadata + * @param actual + */ + protected PersistedFidEntrySubject( + FailureMetadata metadata, @NullableDecl PersistedFidEntry actual) { + super(metadata, actual); + this.actual = actual; + } + + // User-defined test assertion + + public void hasFid(String fid) { + check("getFirebaseInstallationId()").that(actual.getFirebaseInstallationId()).isEqualTo(fid); + } + + public void hasAuthToken(String authToken) { + check("getAuthToken()").that(actual.getAuthToken()).isEqualTo(authToken); + } + + public void hasRefreshToken(String refreshToken) { + check("getRefreshToken()").that(actual.getRefreshToken()).isEqualTo(refreshToken); + } + + public void hasCreationTimestamp(long creationTimestamp) { + check("getTokenCreationEpochInSecs()") + .that(actual.getTokenCreationEpochInSecs()) + .isEqualTo(creationTimestamp); + } + + public void hasTokenExpirationTimestamp(long tokenExpirationTimestamp) { + check("getExpiresInSecs()").that(actual.getExpiresInSecs()).isEqualTo(tokenExpirationTimestamp); + } + + public void hasRegistrationStatus(RegistrationStatus registrationStatus) { + check("getRegistrationStatus()") + .that(actual.getRegistrationStatus()) + .isEqualTo(registrationStatus); + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java new file mode 100644 index 00000000000..f6aa77f45de --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java @@ -0,0 +1,120 @@ +// 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.installations.local; + +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; +import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumented tests for {@link PersistedFid} */ +@RunWith(AndroidJUnit4.class) +public class PersistedFidTest { + + private FirebaseApp firebaseApp0; + private FirebaseApp firebaseApp1; + private PersistedFid persistedFid0; + private PersistedFid persistedFid1; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp0 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId(TEST_APP_ID_1).build()); + firebaseApp1 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId(TEST_APP_ID_2).build(), + "firebase_app_1"); + persistedFid0 = new PersistedFid(firebaseApp0); + persistedFid1 = new PersistedFid(firebaseApp1); + } + + @After + public void cleanUp() throws Exception { + persistedFid0.clear(); + persistedFid1.clear(); + } + + @Test + public void testReadPersistedFidEntry_Null() { + assertNull(persistedFid0.readPersistedFidEntryValue()); + assertNull(persistedFid1.readPersistedFidEntryValue()); + } + + @Test + public void testUpdateAndReadPersistedFidEntry_successful() throws Exception { + // Insert Persisted Fid Entry with Unregistered status in Shared Prefs + assertTrue( + persistedFid0.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())); + PersistedFidEntry entryValue = persistedFid0.readPersistedFidEntryValue(); + + // Validate insertion was successful + assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); + assertThat(entryValue).hasRefreshToken(TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); + assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); + assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_1); + + // Update Persisted Fid Entry with Registered status in Shared Prefs + assertTrue( + persistedFid0.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())); + entryValue = persistedFid0.readPersistedFidEntryValue(); + + // Validate update was successful + assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); + assertThat(entryValue).hasRefreshToken(TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.REGISTERED); + assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); + assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_2); + } +} diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml index 08b562940aa..4b67e67a5a9 100644 --- a/firebase-installations/src/main/AndroidManifest.xml +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + ()), + firebaseApp, + new FirebaseInstallationServiceClient(), + new PersistedFid(firebaseApp), + new Utils()); + } + + FirebaseInstallations( + Clock clock, + Executor executor, + FirebaseApp firebaseApp, + FirebaseInstallationServiceClient serviceClient, + PersistedFid persistedFid, + Utils utils) { + this.clock = clock; this.firebaseApp = firebaseApp; + this.serviceClient = serviceClient; + this.executor = executor; + this.persistedFid = persistedFid; + this.utils = utils; } /** @@ -71,7 +110,9 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { @NonNull @Override public Task getId() { - return Tasks.forResult("fid-is-better-than-iid"); + return Tasks.call(executor, this::getPersistedFid) + .continueWith(orElse(this::createAndPersistNewFid)) + .onSuccessTask(this::registerFidIfNecessary); } /** Returns a auth token(public key) of this Firebase app installation. */ @@ -103,4 +144,108 @@ String getApplicationId() { String getName() { return firebaseApp.getName(); } + + private PersistedFidEntry getPersistedFid() throws FirebaseInstallationsException { + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + if (persistedFidMissingOrInErrorState(persistedFidEntry)) { + throw new FirebaseInstallationsException( + "Failed to get existing fid.", FirebaseInstallationsException.Status.CLIENT_ERROR); + } + return persistedFidEntry; + } + + private static boolean persistedFidMissingOrInErrorState(PersistedFidEntry persistedFidEntry) { + return persistedFidEntry == null + || persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTER_ERROR; + } + + @NonNull + private static Continuation orElse(@NonNull Supplier supplier) { + return t -> { + if (t.isSuccessful()) { + return (T) t.getResult(); + } + return supplier.get(); + }; + } + + private PersistedFidEntry createAndPersistNewFid() throws FirebaseInstallationsException { + String fid = utils.createRandomFid(); + persistFid(fid); + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + return persistedFidEntry; + } + + private void persistFid(String fid) throws FirebaseInstallationsException { + boolean firstUpdateCacheResult = + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.UNREGISTERED) + .build()); + + if (!firstUpdateCacheResult) { + throw new FirebaseInstallationsException( + "Failed to update client side cache.", + FirebaseInstallationsException.Status.CLIENT_ERROR); + } + } + + private Task registerFidIfNecessary(PersistedFidEntry persistedFidEntry) { + String fid = persistedFidEntry.getFirebaseInstallationId(); + + // Check if the fid is unregistered + if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.UNREGISTERED) { + updatePersistedFidWithPendingStatus(fid); + Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); + } + return Tasks.forResult(fid); + } + + private void updatePersistedFidWithPendingStatus(String fid) { + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.PENDING) + .build()); + } + + /** Registers the created Fid with FIS servers and update the shared prefs. */ + private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) + throws FirebaseInstallationsException { + try { + long creationTime = TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); + + InstallationResponse installationResponse = + serviceClient.createFirebaseInstallation( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ persistedFidEntry.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*appId= */ getApplicationId()); + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(installationResponse.getAuthToken().getToken()) + .setRefreshToken(installationResponse.getRefreshToken()) + .setExpiresInSecs( + installationResponse.getAuthToken().getTokenExpirationTimestampMillis()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + + } catch (FirebaseInstallationServiceException exception) { + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) + .build()); + throw new FirebaseInstallationsException( + exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + return null; + } +} + +interface Supplier { + T get() throws Exception; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java new file mode 100644 index 00000000000..fd627a27911 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ +public class FirebaseInstallationsException extends FirebaseException { + + // TODO(ankitagj): Improve clear exception handling. + public enum Status { + SDK_INTERNAL_ERROR, + + CLIENT_ERROR + } + + @NonNull private final Status status; + + public FirebaseInstallationsException(@NonNull Status status) { + this.status = status; + } + + public FirebaseInstallationsException(@NonNull String message, @NonNull Status status) { + super(message); + this.status = status; + } + + public FirebaseInstallationsException( + @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { + super(message, cause); + this.status = status; + } + + /** + * Gets the status for the operation that failed. + * + * @return the status for the FirebaseInstallationsException + */ + @NonNull + public Status getStatus() { + return status; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java new file mode 100644 index 00000000000..1ad7efab2f2 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -0,0 +1,86 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.UUID; + +/** Util methods used for {@link FirebaseInstallations} */ +class Utils { + + /** + * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this + * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy + * Instance-IDs + */ + public static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); + + /** + * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND + * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. + */ + public static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); + + /** Length of new-format FIDs as introduced in 2019. */ + public static final int FID_LENGTH = 22; + + /** + * Creates a random FID of valid format without checking if the FID is already in use by any + * Firebase Installation. + * + *

Note: Even though this method does not check with the FIS database if the returned FID is + * already in use, the probability of collision is extremely and negligibly small! + * + * @return random FID value + */ + @NonNull + public String createRandomFid() { + // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. + byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); + uuidBytes[16] = uuidBytes[0]; + uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); + return encodeFidBase64UrlSafe(uuidBytes); + } + + /** + * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded + * String-representation. + * + *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is + * recommended to deliver a byte-array containing at least 16.5 bytes. + * + * @param rawValue FID value to be encoded + * @return (22-character or shorter) String containing the base64-encoded value + */ + private static String encodeFidBase64UrlSafe(byte[] rawValue) { + return new String( + android.util.Base64.encode( + rawValue, + android.util.Base64.URL_SAFE + | android.util.Base64.NO_PADDING + | android.util.Base64.NO_WRAP), + Charset.defaultCharset()) + .substring(0, FID_LENGTH); + } + + private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { + ByteBuffer bb = ByteBuffer.wrap(output); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java deleted file mode 100644 index 3d42ca15bbf..00000000000 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java +++ /dev/null @@ -1,59 +0,0 @@ -// 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.installations.local; - -import androidx.annotation.NonNull; -import com.google.auto.value.AutoValue; - -/** - * This class represents a cache entry value in {@link FiidCache}, which contains a Firebase - * instance id and the cache status of this entry. - */ -@AutoValue -public abstract class FiidCacheEntryValue { - - @NonNull - public abstract String getFirebaseInstallationId(); - - @NonNull - public abstract FiidCache.CacheStatus getCacheStatus(); - - @NonNull - public abstract String getAuthToken(); - - @NonNull - public abstract String getRefreshToken(); - - public abstract long getExpiresInSecs(); - - public abstract long getTokenCreationEpochInSecs(); - - @NonNull - public static FiidCacheEntryValue create( - @NonNull String firebaseInstallationId, - @NonNull FiidCache.CacheStatus cacheStatus, - @NonNull String authToken, - @NonNull String refreshToken, - long tokenCreationEpochInSecs, - long expiresInSecs) { - return new AutoValue_FiidCacheEntryValue( - firebaseInstallationId, - cacheStatus, - authToken, - refreshToken, - expiresInSecs, - tokenCreationEpochInSecs); - } -} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java similarity index 58% rename from firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java rename to firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java index 589c9fb9266..177c94fd0ad 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java @@ -19,39 +19,50 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.FirebaseApp; +import java.util.Arrays; +import java.util.List; /** - * A layer that locally caches a few Firebase Installation attributes on top the Firebase - * Installation backend API. + * A layer that locally persists a few Firebase Installation attributes on top the Firebase + * Installation API. */ -public class FiidCache { - // 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. - public enum CacheStatus { - // Cache entry is synced to Firebase backend +public class PersistedFid { + // Registration Status of each persisted fid entry + // NOTE: never change the ordinal of the enum values because the enum values are stored in shared + // prefs as their ordinal numbers. + public enum RegistrationStatus { + /** {@link PersistedFidEntry} is synced to FIS servers */ REGISTERED, - // Cache entry is waiting for Firebase backend response or internal network retry + /** {@link PersistedFidEntry} is not synced with FIS server */ UNREGISTERED, - // Cache entry is in error state when syncing with Firebase backend + /** {@link PersistedFidEntry} is in error state when syncing with FIS server */ REGISTER_ERROR, - // Cache entry is in delete state before syncing with Firebase backend - DELETED + /** {@link PersistedFidEntry} is in pending state when waiting for FIS server response */ + PENDING } - private static final String SHARED_PREFS_NAME = "FiidCache"; + private static final String SHARED_PREFS_NAME = "PersistedFid"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; private static final String REFRESH_TOKEN_KEY = "RefreshToken"; private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; - private static final String CACHE_STATUS_KEY = "Status"; + private static final String PERSISTED_STATUS_KEY = "Status"; + + private static final List FID_PREF_KEYS = + Arrays.asList( + FIREBASE_INSTALLATION_ID_KEY, + AUTH_TOKEN_KEY, + REFRESH_TOKEN_KEY, + TOKEN_CREATION_TIME_IN_SECONDS_KEY, + EXPIRES_IN_SECONDS_KEY, + PERSISTED_STATUS_KEY); private final SharedPreferences prefs; private final String persistenceKey; - public FiidCache(@NonNull FirebaseApp firebaseApp) { + public PersistedFid(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path prefs = @@ -62,30 +73,41 @@ public FiidCache(@NonNull FirebaseApp firebaseApp) { } @Nullable - public synchronized FiidCacheEntryValue readCacheEntryValue() { - String iid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); + public synchronized PersistedFidEntry readPersistedFidEntryValue() { + String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); long tokenCreationTime = prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - if (iid == null || status == -1) { + if (fid == null + || status == -1 + || !(status >= 0 && status < RegistrationStatus.values().length)) { return null; } - return FiidCacheEntryValue.create( - iid, CacheStatus.values()[status], authToken, refreshToken, tokenCreationTime, expiresIn); + return PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .build(); } @NonNull - public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValue entryValue) { + public synchronized boolean insertOrUpdatePersistedFidEntry( + @NonNull PersistedFidEntry entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), entryValue.getFirebaseInstallationId()); - editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); + editor.putInt( + getSharedPreferencesKey(PERSISTED_STATUS_KEY), + entryValue.getRegistrationStatus().ordinal()); editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); editor.putLong( @@ -98,12 +120,10 @@ public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValu @NonNull public synchronized boolean clear() { SharedPreferences.Editor editor = prefs.edit(); - editor.remove(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY)); - editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); - editor.remove(getSharedPreferencesKey(AUTH_TOKEN_KEY)); - editor.remove(getSharedPreferencesKey(REFRESH_TOKEN_KEY)); - editor.remove(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY)); - editor.remove(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY)); + for (String k : FID_PREF_KEYS) { + editor.remove(getSharedPreferencesKey(k)); + } + editor.commit(); return editor.commit(); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java new file mode 100644 index 00000000000..4c8a9a56ec4 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java @@ -0,0 +1,78 @@ +// 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.installations.local; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + +/** + * This class represents a persisted fid entry in {@link PersistedFid}, which contains a few + * Firebase Installation attributes and the persisted status of this entry. + */ +@AutoValue +public abstract class PersistedFidEntry { + + @NonNull + public abstract String getFirebaseInstallationId(); + + @NonNull + public abstract PersistedFid.RegistrationStatus getRegistrationStatus(); + + @Nullable + public abstract String getAuthToken(); + + @Nullable + public abstract String getRefreshToken(); + + public abstract long getExpiresInSecs(); + + public abstract long getTokenCreationEpochInSecs(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an PersistedFidEntry object */ + @NonNull + public static PersistedFidEntry.Builder builder() { + return new AutoValue_PersistedFidEntry.Builder() + .setTokenCreationEpochInSecs(0) + .setExpiresInSecs(0); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setFirebaseInstallationId(@NonNull String value); + + @NonNull + public abstract Builder setRegistrationStatus(@NonNull PersistedFid.RegistrationStatus value); + + @NonNull + public abstract Builder setAuthToken(@Nullable String value); + + @NonNull + public abstract Builder setRefreshToken(@Nullable String value); + + @NonNull + public abstract Builder setExpiresInSecs(long value); + + @NonNull + public abstract Builder setTokenCreationEpochInSecs(long value); + + @NonNull + public abstract PersistedFidEntry build(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 51cf2e1a219..c0105a8607b 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -32,10 +32,10 @@ public class FirebaseInstallationServiceClient { "firebaseinstallations.googleapis.com"; private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations"; private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT = - "projects/%s/installations/%s/auth:generate"; + "projects/%s/installations/%s/authTokens:generate"; private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s"; private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1"; - private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_V2"; + private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_v2"; private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; private static final String ACCEPT_HEADER_KEY = "Accept"; @@ -48,14 +48,20 @@ public class FirebaseInstallationServiceClient { private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + /** + * Creates a FID on the FIS Servers by calling FirebaseInstallations API create method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param appId the identifier of a Firebase application + * @return {@link InstallationResponse} generated from the response body + */ @NonNull public InstallationResponse createFirebaseInstallation( - long projectNumber, - @NonNull String apiKey, - @NonNull String firebaseInstallationId, - @NonNull String appId) + @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) throws FirebaseInstallationServiceException { - String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber); + String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); try { URL url = new URL( @@ -76,9 +82,7 @@ public InstallationResponse createFirebaseInstallation( new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(firebaseInstallationId, appId) - .toString() - .getBytes("UTF-8")); + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); } catch (JSONException e) { throw new IllegalStateException(e); } finally { @@ -91,16 +95,16 @@ public InstallationResponse createFirebaseInstallation( return readCreateResponse(httpsURLConnection); case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } @@ -109,15 +113,26 @@ private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, JSONObject firebaseInstallationData = new JSONObject(); firebaseInstallationData.put("fid", fid); firebaseInstallationData.put("appId", appId); - firebaseInstallationData.put("appVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + firebaseInstallationData.put("authVersion", FIREBASE_INSTALLATION_AUTH_VERSION); return firebaseInstallationData; } + /** + * Deletes a FID on the FIS Servers by calling FirebaseInstallations API delete method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param refreshToken a token used to authenticate FIS requests + */ @NonNull public void deleteFirebaseInstallation( - long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + @NonNull String apiKey, + @NonNull String fid, + @NonNull String projectID, + @NonNull String refreshToken) throws FirebaseInstallationServiceException { - String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = new URL( @@ -131,7 +146,7 @@ public void deleteFirebaseInstallation( HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("DELETE"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); @@ -141,25 +156,37 @@ public void deleteFirebaseInstallation( return; case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } + /** + * Generates a new auth token for a FID on the FIS Servers by calling FirebaseInstallations API + * generateAuthToken method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param refreshToken a token used to authenticate FIS requests + */ @NonNull public InstallationTokenResult generateAuthToken( - long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + @NonNull String apiKey, + @NonNull String fid, + @NonNull String projectID, + @NonNull String refreshToken) throws FirebaseInstallationServiceException { String resourceName = - String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = new URL( @@ -173,7 +200,7 @@ public InstallationTokenResult generateAuthToken( HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); @@ -184,16 +211,16 @@ public InstallationTokenResult generateAuthToken( return readGenerateAuthTokenResponse(httpsURLConnection); case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } // Read the response from the createFirebaseInstallation API. @@ -214,7 +241,7 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws while (reader.hasNext()) { String key = reader.nextName(); if (key.equals("token")) { - installationTokenResult.setAuthToken(reader.nextString()); + installationTokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); } else { @@ -242,7 +269,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("token")) { - builder.setAuthToken(reader.nextString()); + builder.setToken(reader.nextString()); } else if (name.equals("expiresIn")) { builder.setTokenExpirationTimestampMillis(reader.nextLong()); } else { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java index b703b56d539..b9be3727d4d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java @@ -19,8 +19,7 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ public class FirebaseInstallationServiceException extends FirebaseException { - - public enum Code { + public enum Status { SERVER_ERROR, NETWORK_ERROR, @@ -28,30 +27,30 @@ public enum Code { UNAUTHORIZED } - @NonNull private final Code code; + @NonNull private final Status status; - FirebaseInstallationServiceException(@NonNull Code code) { - this.code = code; + public FirebaseInstallationServiceException(@NonNull Status status) { + this.status = status; } - FirebaseInstallationServiceException(@NonNull String message, @NonNull Code code) { + public FirebaseInstallationServiceException(@NonNull String message, @NonNull Status status) { super(message); - this.code = code; + this.status = status; } - FirebaseInstallationServiceException( - @NonNull String message, @NonNull Code code, Throwable cause) { + public FirebaseInstallationServiceException( + @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { super(message, cause); - this.code = code; + this.status = status; } /** - * Gets the status code for the operation that failed. + * Gets the status status for the operation that failed. * - * @return the code for the FirebaseInstallationServiceException + * @return the status for the FirebaseInstallationServiceException */ @NonNull - public Code getCode() { - return code; + public Status getStatus() { + return status; } } diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java index 5a7c505e9b6..f553b7d701a 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -14,5 +14,42 @@ package com.google.firebase.installations; +import static org.junit.Assert.assertNotNull; + +import androidx.test.core.app.ApplicationProvider; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + /** Tests for {@link FirebaseInstallationsRegistrar}. */ -public class FirebaseInstallationsRegistrarTest {} +@RunWith(RobolectricTestRunner.class) +public class FirebaseInstallationsRegistrarTest { + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + } + + @Test + public void getFirebaseInstallationsInstance() { + FirebaseApp defaultApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + + FirebaseApp anotherApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + + FirebaseInstallations defaultFirebaseInstallation = FirebaseInstallations.getInstance(); + assertNotNull(defaultFirebaseInstallation); + + FirebaseInstallations anotherFirebaseInstallation = + FirebaseInstallations.getInstance(anotherApp); + assertNotNull(anotherFirebaseInstallation); + } +} From 19803214f67cd118ad1d0dd53b0b84db7ac580cc Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 24 Jul 2019 11:24:14 -0700 Subject: [PATCH 47/74] Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. --- .../firebase-installations-interop.gradle | 46 ++++++++ .../gradle.properties | 1 + .../src/main/AndroidManifest.xml | 17 +++ .../FirebaseInstallationsApi.java | 42 +++++++ .../InstallationTokenResult.java | 38 +++++++ .../firebase-installations.gradle | 54 +++++++++ firebase-installations/gradle.properties | 1 + firebase-installations/lint.xml | 11 ++ .../src/main/AndroidManifest.xml | 14 +++ .../installations/FirebaseInstallations.java | 106 ++++++++++++++++++ .../FirebaseInstallationsRegistrar.java | 39 +++++++ .../FirebaseInstallationsRegistrarTest.java | 18 +++ .../FirebaseInstallationsTest.java | 18 +++ subprojects.cfg | 2 + 14 files changed, 407 insertions(+) create mode 100644 firebase-installations-interop/firebase-installations-interop.gradle create mode 100644 firebase-installations-interop/gradle.properties create mode 100644 firebase-installations-interop/src/main/AndroidManifest.xml create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java create mode 100644 firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java create mode 100644 firebase-installations/firebase-installations.gradle create mode 100644 firebase-installations/gradle.properties create mode 100644 firebase-installations/lint.xml create mode 100644 firebase-installations/src/main/AndroidManifest.xml create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java diff --git a/firebase-installations-interop/firebase-installations-interop.gradle b/firebase-installations-interop/firebase-installations-interop.gradle new file mode 100644 index 00000000000..5acad3e1434 --- /dev/null +++ b/firebase-installations-interop/firebase-installations-interop.gradle @@ -0,0 +1,46 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +firebaseLibrary.publishJavadoc = false + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" + annotationProcessor "com.google.auto.value:auto-value:1.6.2" +} diff --git a/firebase-installations-interop/gradle.properties b/firebase-installations-interop/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations-interop/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations-interop/src/main/AndroidManifest.xml b/firebase-installations-interop/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7ae18eafe43 --- /dev/null +++ b/firebase-installations-interop/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java new file mode 100644 index 00000000000..6947d7ec037 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -0,0 +1,42 @@ +// 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.installations; + +import com.google.android.gms.tasks.Task; + +/** + * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component + * injection. + * + * @hide + */ +public interface FirebaseInstallationsApi { + + /** + * Async function that returns a globally unique identifier of this Firebase app installation. + * This is a url-safe base64 string of a 128-bit integer. + */ + Task getId(); + + /** Async function that returns a auth token(public key) of this Firebase app installation. */ + Task getAuthToken(boolean forceRefresh); + + /** + * Async function that deletes this Firebase app installation from Firebase backend. This call + * would possibly lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or + * Firebase In-App Messaging not function properly. + */ + Task delete(); +} diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java new file mode 100644 index 00000000000..df04f26ca38 --- /dev/null +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -0,0 +1,38 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class InstallationTokenResult { + + /** A new FIS Auth-Token, created for this firebase installation. */ + @NonNull + public abstract String getAuthToken(); + /** + * The amount of time, in milliseconds, before the auth-token expires for this firebase + * installation. + */ + public abstract long getTokenExpirationTimestampMillis(); + + @NonNull + public static InstallationTokenResult create( + @NonNull String authToken, long tokenExpirationTimestampMillis) { + return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + } +} diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle new file mode 100644 index 00000000000..45123f1c6ce --- /dev/null +++ b/firebase-installations/firebase-installations.gradle @@ -0,0 +1,54 @@ +// 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. + + +plugins { + id 'firebase-library' +} + +android { + compileSdkVersion project.targetSdkVersion + defaultConfig { + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + multiDexEnabled true + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + testOptions { + unitTests { + includeAndroidResources = true + } + } +} + +dependencies { + implementation project(':firebase-common') + implementation project(':firebase-installations-interop') + + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + testImplementation 'androidx.test:core:1.2.0' + testImplementation 'junit:junit:4.12' + testImplementation "org.robolectric:robolectric:$robolectricVersion" + + androidTestImplementation 'androidx.test:runner:1.2.0' + implementation 'com.google.guava:guava:16.0.+' +} diff --git a/firebase-installations/gradle.properties b/firebase-installations/gradle.properties new file mode 100644 index 00000000000..752913a3eb5 --- /dev/null +++ b/firebase-installations/gradle.properties @@ -0,0 +1 @@ +version=17.1.1 diff --git a/firebase-installations/lint.xml b/firebase-installations/lint.xml new file mode 100644 index 00000000000..9b9bd90b534 --- /dev/null +++ b/firebase-installations/lint.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..08b562940aa --- /dev/null +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java new file mode 100644 index 00000000000..f135aa2e197 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -0,0 +1,106 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.FirebaseApp; + +/** + * Entry point for Firebase Installations. + * + *

Firebase Installations does + * + *

    + *
  • provide unique identifier for a Firebase installation + *
  • provide auth token of a Firebase installation + *
  • provide a API to GDPR-delete a Firebase installation + *
+ */ +public class FirebaseInstallations implements FirebaseInstallationsApi { + + private final FirebaseApp firebaseApp; + + /** package private constructor. */ + FirebaseInstallations(FirebaseApp firebaseApp) { + this.firebaseApp = firebaseApp; + } + + /** + * Returns the {@link FirebaseInstallationsApi} initialized with the default {@link FirebaseApp}. + * + * @return a {@link FirebaseInstallationsApi} instance + */ + @NonNull + public static FirebaseInstallations getInstance() { + FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); + return getInstance(defaultFirebaseApp); + } + + /** + * Returns the {@link FirebaseInstallations} initialized with a custom {@link FirebaseApp}. + * + * @param app a custom {@link FirebaseApp} + * @return a {@link FirebaseInstallations} instance + */ + @NonNull + public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { + Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); + return (FirebaseInstallations) app.get(FirebaseInstallationsApi.class); + } + + /** + * Returns a globally unique identifier of this Firebase app installation. This is a url-safe + * base64 string of a 128-bit integer. + */ + @NonNull + @Override + public Task getId() { + return Tasks.forResult("fid-is-better-than-iid"); + } + + /** Returns a auth token(public key) of this Firebase app installation. */ + @NonNull + @Override + public Task getAuthToken(boolean forceRefresh) { + return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + } + + /** + * Call to delete this Firebase app installation from Firebase backend. This call would possibly + * lead Firebase Notification, Firebase RemoteConfig, Firebase Predictions or Firebase In-App + * Messaging not function properly. + */ + @NonNull + @Override + public Task delete() { + return Tasks.forResult(null); + } + + /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getApplicationId() { + return firebaseApp.getOptions().getApplicationId(); + } + + /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getName() { + return firebaseApp.getName(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java new file mode 100644 index 00000000000..e84168ab4fb --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java @@ -0,0 +1,39 @@ +// 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.installations; + +import androidx.annotation.Keep; +import com.google.firebase.FirebaseApp; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** @hide */ +@Keep +public class FirebaseInstallationsRegistrar implements ComponentRegistrar { + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseInstallationsApi.class) + .add(Dependency.required(FirebaseApp.class)) + .factory(c -> new FirebaseInstallations(c.get(FirebaseApp.class))) + .build(), + LibraryVersionComponent.create("fire-installations", BuildConfig.VERSION_NAME)); + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java new file mode 100644 index 00000000000..5a7c505e9b6 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallationsRegistrar}. */ +public class FirebaseInstallationsRegistrarTest {} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java new file mode 100644 index 00000000000..fe64dcf06b8 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -0,0 +1,18 @@ +// 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.installations; + +/** Tests for {@link FirebaseInstallations}. */ +public class FirebaseInstallationsTest {} diff --git a/subprojects.cfg b/subprojects.cfg index a80ebdb3c42..031fb302706 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -12,6 +12,8 @@ firebase-firestore firebase-firestore:ktx firebase-functions firebase-functions:ktx +firebase-installations-interop +firebase-installations firebase-inappmessaging-display firebase-storage firebase-storage:test-app From af1e510b0f91020e45fc75b476e37d998f2a69a0 Mon Sep 17 00:00:00 2001 From: Ankita Date: Tue, 30 Jul 2019 10:05:39 -0700 Subject: [PATCH 48/74] Adding http client to call fis backend service (#659) * Adding Firebase Installations module * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Http client to call FIS backend service. * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. * Http client to call FIS backend service. * Addresing comments and introducing new FirebaseInstallationService Exception. --- .../InstallationTokenResult.java | 22 +- .../firebase-installations.gradle | 4 + .../installations/FirebaseInstallations.java | 2 +- .../FirebaseInstallationServiceClient.java | 256 ++++++++++++++++++ .../FirebaseInstallationServiceException.java | 57 ++++ .../remote/InstallationResponse.java | 56 ++++ 6 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index df04f26ca38..5e2450b14d6 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -28,11 +28,27 @@ public abstract class InstallationTokenResult { * The amount of time, in milliseconds, before the auth-token expires for this firebase * installation. */ + @NonNull public abstract long getTokenExpirationTimestampMillis(); @NonNull - public static InstallationTokenResult create( - @NonNull String authToken, long tokenExpirationTimestampMillis) { - return new AutoValue_InstallationTokenResult(authToken, tokenExpirationTimestampMillis); + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationTokenResult.Builder builder() { + return new AutoValue_InstallationTokenResult.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setAuthToken(@NonNull String value); + + @NonNull + public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); + + @NonNull + public abstract InstallationTokenResult build(); } } diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 45123f1c6ce..ec0224515dd 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -45,6 +45,10 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.0' + + 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" diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index f135aa2e197..e33664b6e32 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -78,7 +78,7 @@ public Task getId() { @NonNull @Override public Task getAuthToken(boolean forceRefresh) { - return Tasks.forResult(InstallationTokenResult.create("dummy_auth_token", 1000l)); + return Tasks.forResult(InstallationTokenResult.builder().build()); } /** diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java new file mode 100644 index 00000000000..51cf2e1a219 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -0,0 +1,256 @@ +// 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.installations.remote; + +import android.util.JsonReader; +import androidx.annotation.NonNull; +import com.google.firebase.installations.InstallationTokenResult; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HttpsURLConnection; +import org.json.JSONException; +import org.json.JSONObject; + +/** Http client that sends request to Firebase Installations backend API. */ +public class FirebaseInstallationServiceClient { + private static final String FIREBASE_INSTALLATIONS_API_DOMAIN = + "firebaseinstallations.googleapis.com"; + private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations"; + private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT = + "projects/%s/installations/%s/auth:generate"; + private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s"; + private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1"; + private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_V2"; + + private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; + private static final String ACCEPT_HEADER_KEY = "Accept"; + private static final String JSON_CONTENT_TYPE = "application/json"; + private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; + private static final String GZIP_CONTENT_ENCODING = "gzip"; + + private static final String UNAUTHORIZED_ERROR_MESSAGE = + "The request did not have the required credentials."; + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; + private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + + @NonNull + public InstallationResponse createFirebaseInstallation( + long projectNumber, + @NonNull String apiKey, + @NonNull String firebaseInstallationId, + @NonNull String appId) + throws FirebaseInstallationServiceException { + String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(firebaseInstallationId, appId) + .toString() + .getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readCreateResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, String appId) + throws JSONException { + JSONObject firebaseInstallationData = new JSONObject(); + firebaseInstallationData.put("fid", fid); + firebaseInstallationData.put("appId", appId); + firebaseInstallationData.put("appVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + return firebaseInstallationData; + } + + @NonNull + public void deleteFirebaseInstallation( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("DELETE"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return; + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + + @NonNull + public InstallationTokenResult generateAuthToken( + long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + throws FirebaseInstallationServiceException { + String resourceName = + String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + try { + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setDoOutput(true); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readGenerateAuthTokenResponse(httpsURLConnection); + case 401: + throw new FirebaseInstallationServiceException( + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + default: + throw new FirebaseInstallationServiceException( + INTERNAL_SERVER_ERROR_MESSAGE, + FirebaseInstallationServiceException.Code.SERVER_ERROR); + } + } catch (IOException e) { + throw new FirebaseInstallationServiceException( + NETWORK_ERROR_MESSAGE + e.getMessage(), + FirebaseInstallationServiceException.Code.NETWORK_ERROR); + } + } + // Read the response from the createFirebaseInstallation API. + private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); + InstallationResponse.Builder builder = InstallationResponse.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("name")) { + builder.setName(reader.nextString()); + } else if (name.equals("refreshToken")) { + builder.setRefreshToken(reader.nextString()); + } else if (name.equals("authToken")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (key.equals("token")) { + installationTokenResult.setAuthToken(reader.nextString()); + } else if (key.equals("expiresIn")) { + installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + builder.setAuthToken(installationTokenResult.build()); + reader.endObject(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } + + // Read the response from the generateAuthToken FirebaseInstallation API. + private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) + throws IOException { + JsonReader reader = + new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("token")) { + builder.setAuthToken(reader.nextString()); + } else if (name.equals("expiresIn")) { + builder.setTokenExpirationTimestampMillis(reader.nextLong()); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + return builder.build(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java new file mode 100644 index 00000000000..b703b56d539 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java @@ -0,0 +1,57 @@ +// 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.installations.remote; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ +public class FirebaseInstallationServiceException extends FirebaseException { + + public enum Code { + SERVER_ERROR, + + NETWORK_ERROR, + + UNAUTHORIZED + } + + @NonNull private final Code code; + + FirebaseInstallationServiceException(@NonNull Code code) { + this.code = code; + } + + FirebaseInstallationServiceException(@NonNull String message, @NonNull Code code) { + super(message); + this.code = code; + } + + FirebaseInstallationServiceException( + @NonNull String message, @NonNull Code code, Throwable cause) { + super(message, cause); + this.code = code; + } + + /** + * Gets the status code for the operation that failed. + * + * @return the code for the FirebaseInstallationServiceException + */ + @NonNull + public Code getCode() { + return code; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java new file mode 100644 index 00000000000..2022a498fe1 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations.remote; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; +import com.google.firebase.installations.InstallationTokenResult; + +@AutoValue +public abstract class InstallationResponse { + + @NonNull + public abstract String getName(); + + @NonNull + public abstract String getRefreshToken(); + + @NonNull + public abstract InstallationTokenResult getAuthToken(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static InstallationResponse.Builder builder() { + return new AutoValue_InstallationResponse.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setName(@NonNull String value); + + @NonNull + public abstract Builder setRefreshToken(@NonNull String value); + + @NonNull + public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + + @NonNull + public abstract InstallationResponse build(); + } +} From 30b48c3bb7976f8873517ffbae145c8cc07bcddc Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 7 Aug 2019 14:05:07 -0700 Subject: [PATCH 49/74] Implementing cache for FIS SDK (#694) * Implementing cache for FIS SDK * Implementing cache for FIS SDK * Addressing Di's comments. * Addressing Di's comments. --- .../firebase-installations.gradle | 5 +- .../src/androidTest/AndroidManifest.xml | 26 ++++ .../installation/local/FiidCacheTest.java | 110 +++++++++++++++++ .../installations/FirebaseInstallations.java | 2 +- .../installations/local/FiidCache.java | 113 ++++++++++++++++++ .../local/FiidCacheEntryValue.java | 59 +++++++++ 6 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 firebase-installations/src/androidTest/AndroidManifest.xml create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index ec0224515dd..c5fea1c252b 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -53,6 +53,9 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' - implementation 'com.google.guava:guava:16.0.+' + androidTestImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'junit:junit:4.12' + androidTestImplementation "androidx.annotation:annotation:1.0.0" } diff --git a/firebase-installations/src/androidTest/AndroidManifest.xml b/firebase-installations/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000000..43c54f991fd --- /dev/null +++ b/firebase-installations/src/androidTest/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java new file mode 100644 index 00000000000..c43a6729610 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java @@ -0,0 +1,110 @@ +// 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.installation.local; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.FiidCache; +import com.google.firebase.installations.local.FiidCacheEntryValue; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumented tests for {@link FiidCache} */ +@RunWith(AndroidJUnit4.class) +public class FiidCacheTest { + + private FirebaseApp firebaseApp0; + private FirebaseApp firebaseApp1; + private FiidCache cache0; + private FiidCache cache1; + private final String AUTH_TOKEN = "auth_token"; + private final String REFRESH_TOKEN = "refresh_token"; + + private final long TIMESTAMP_IN_SECONDS = 100L; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp0 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + firebaseApp1 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + cache0 = new FiidCache(firebaseApp0); + cache1 = new FiidCache(firebaseApp1); + } + + @After + public void cleanUp() throws Exception { + cache0.clear(); + cache1.clear(); + } + + @Test + public void testReadCacheEntry_Null() { + assertNull(cache0.readCacheEntryValue()); + assertNull(cache1.readCacheEntryValue()); + } + + @Test + public void testUpdateAndReadCacheEntry() throws Exception { + assertTrue( + cache0.insertOrUpdateCacheEntry( + FiidCacheEntryValue.create( + "123456", + FiidCache.CacheStatus.UNREGISTERED, + AUTH_TOKEN, + REFRESH_TOKEN, + TIMESTAMP_IN_SECONDS, + TIMESTAMP_IN_SECONDS))); + FiidCacheEntryValue entryValue = cache0.readCacheEntryValue(); + assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); + assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); + assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.UNREGISTERED); + assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertNull(cache1.readCacheEntryValue()); + + assertTrue( + cache0.insertOrUpdateCacheEntry( + FiidCacheEntryValue.create( + "123456", + FiidCache.CacheStatus.REGISTERED, + AUTH_TOKEN, + REFRESH_TOKEN, + 200L, + TIMESTAMP_IN_SECONDS))); + entryValue = cache0.readCacheEntryValue(); + assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); + assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); + assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); + assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTERED); + assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); + assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(200L); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index e33664b6e32..75affe7e15f 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -15,10 +15,10 @@ package com.google.firebase.installations; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; -import com.google.common.annotations.VisibleForTesting; import com.google.firebase.FirebaseApp; /** diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java new file mode 100644 index 00000000000..589c9fb9266 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java @@ -0,0 +1,113 @@ +// 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.installations.local; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.FirebaseApp; + +/** + * A layer that locally caches a few Firebase Installation attributes on top the Firebase + * Installation backend API. + */ +public class FiidCache { + // 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. + public enum CacheStatus { + // Cache entry is synced to Firebase backend + REGISTERED, + // Cache entry is waiting for Firebase backend response or internal network retry + UNREGISTERED, + // Cache entry is in error state when syncing with Firebase backend + REGISTER_ERROR, + // Cache entry is in delete state before syncing with Firebase backend + DELETED + } + + private static final String SHARED_PREFS_NAME = "FiidCache"; + + private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; + private static final String AUTH_TOKEN_KEY = "AuthToken"; + private static final String REFRESH_TOKEN_KEY = "RefreshToken"; + private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; + private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; + private static final String CACHE_STATUS_KEY = "Status"; + + private final SharedPreferences prefs; + private final String persistenceKey; + + public FiidCache(@NonNull FirebaseApp firebaseApp) { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path + prefs = + firebaseApp + .getApplicationContext() + .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); + persistenceKey = firebaseApp.getPersistenceKey(); + } + + @Nullable + public synchronized FiidCacheEntryValue readCacheEntryValue() { + String iid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); + String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); + String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); + long tokenCreationTime = + prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); + long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + + if (iid == null || status == -1) { + return null; + } + + return FiidCacheEntryValue.create( + iid, CacheStatus.values()[status], authToken, refreshToken, tokenCreationTime, expiresIn); + } + + @NonNull + public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValue entryValue) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString( + getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), + entryValue.getFirebaseInstallationId()); + editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); + editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); + editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); + editor.putLong( + getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), + entryValue.getTokenCreationEpochInSecs()); + editor.putLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); + return editor.commit(); + } + + @NonNull + public synchronized boolean clear() { + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY)); + editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); + editor.remove(getSharedPreferencesKey(AUTH_TOKEN_KEY)); + editor.remove(getSharedPreferencesKey(REFRESH_TOKEN_KEY)); + editor.remove(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY)); + editor.remove(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY)); + return editor.commit(); + } + + private String getSharedPreferencesKey(String key) { + return String.format("%s|%s", persistenceKey, key); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java new file mode 100644 index 00000000000..3d42ca15bbf --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java @@ -0,0 +1,59 @@ +// 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.installations.local; + +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** + * This class represents a cache entry value in {@link FiidCache}, which contains a Firebase + * instance id and the cache status of this entry. + */ +@AutoValue +public abstract class FiidCacheEntryValue { + + @NonNull + public abstract String getFirebaseInstallationId(); + + @NonNull + public abstract FiidCache.CacheStatus getCacheStatus(); + + @NonNull + public abstract String getAuthToken(); + + @NonNull + public abstract String getRefreshToken(); + + public abstract long getExpiresInSecs(); + + public abstract long getTokenCreationEpochInSecs(); + + @NonNull + public static FiidCacheEntryValue create( + @NonNull String firebaseInstallationId, + @NonNull FiidCache.CacheStatus cacheStatus, + @NonNull String authToken, + @NonNull String refreshToken, + long tokenCreationEpochInSecs, + long expiresInSecs) { + return new AutoValue_FiidCacheEntryValue( + firebaseInstallationId, + cacheStatus, + authToken, + refreshToken, + expiresInSecs, + tokenCreationEpochInSecs); + } +} From 05da0b056d557b170489d92d25c46ef793643bfd Mon Sep 17 00:00:00 2001 From: Ankita Date: Fri, 30 Aug 2019 14:06:10 -0700 Subject: [PATCH 50/74] Adding Util class for FIrebaseInstallations APIs. (#676) * Adding Firebase Installations module * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Http client to call FIS backend service. * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Http client to call FIS backend service. * Http client to call FIS backend service. * Initial Code structure for FIS Android SDK (#648) * Adding an interface library for Firebase Installations SDK * Adding Firebase Installations module * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Adding Firebase Installations module. * Readding .idea files that were deleted in previous commit * Revert "Adding Firebase Installations module" This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Revert "Readding .idea files that were deleted in previous commit" This reverts commit 7b4ebcf7accb28fc01ddd5e864fa3e9c0d46dba5. * Add firebase installations project path * Fixing formattinf issues. * Revert "Adding Firebase Installations module" with hidden files This reverts commit 2ec4aeff1e96446ad4737e0b4f75e24fa63eba55. * Addressing review comments. * Making InstallationTokenResult an AutoValue class. * Http client to call FIS backend service. * Addresing comments and introducing new FirebaseInstallationService Exception. * Implementing getId method in FirebaseInstallation to call backend. * 1. Adding instrumentation tests. 2. Introducing serviceClient level and main class exceptions. * Addressing Di's comments. * Addressing Rayo's comments * Updating parameters order in ServiceClient. * Updating createRandomFid as per Rayo's comments. * getId() implementation with instrumented tests. (#703) * getId() implementation with instrumented tests. * Addressing rayo's comments. - Detailed JavaDocs - Renaming FiidCache as PersistedFid * Addressing rayo's comments. - Detailed JavaDocs - Renaming FiidCache as PersistedFid * Addresing comments to resoleve the following: - Make registerAnsSaveFid non blocking call in getId() - PersistedFidEntry builder with default values * Addressing Ciaran and Rayo's comments. * Addressing Ciaran's comments * Addressing Ciaran's comments * Adding param comments and checking if registration status is valid. * Correcting lint warning: uses-permission should be declared before application in AndroidManifest.xml * Adding custom assertThat with more readable assertions * Correcting instrumented tests to be reliable. --- .../InstallationTokenResult.java | 10 +- .../firebase-installations.gradle | 2 + .../src/androidTest/AndroidManifest.xml | 2 +- .../installation/local/FiidCacheTest.java | 110 --------- ...FirebaseInstallationsInstrumentedTest.java | 220 ++++++++++++++++++ .../FisAndroidTestConstants.java | 33 +++ .../local/PersistedFidEntrySubject.java | 85 +++++++ .../installations/local/PersistedFidTest.java | 120 ++++++++++ .../src/main/AndroidManifest.xml | 3 +- .../installations/FirebaseInstallations.java | 147 +++++++++++- .../FirebaseInstallationsException.java | 56 +++++ .../google/firebase/installations/Utils.java | 86 +++++++ .../local/FiidCacheEntryValue.java | 59 ----- .../{FiidCache.java => PersistedFid.java} | 78 ++++--- .../local/PersistedFidEntry.java | 78 +++++++ .../FirebaseInstallationServiceClient.java | 83 ++++--- .../FirebaseInstallationServiceException.java | 27 ++- .../FirebaseInstallationsRegistrarTest.java | 39 +++- 18 files changed, 989 insertions(+), 249 deletions(-) delete mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/Utils.java delete mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java rename firebase-installations/src/main/java/com/google/firebase/installations/local/{FiidCache.java => PersistedFid.java} (58%) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index 5e2450b14d6..5cd761299eb 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -21,12 +21,12 @@ @AutoValue public abstract class InstallationTokenResult { - /** A new FIS Auth-Token, created for this firebase installation. */ + /** A new FIS Auth-Token, created for this Firebase Installation. */ @NonNull - public abstract String getAuthToken(); + public abstract String getToken(); /** - * The amount of time, in milliseconds, before the auth-token expires for this firebase - * installation. + * The amount of time, in milliseconds, before the auth-token expires for this Firebase + * Installation. */ @NonNull public abstract long getTokenExpirationTimestampMillis(); @@ -43,7 +43,7 @@ public static InstallationTokenResult.Builder builder() { @AutoValue.Builder public abstract static class Builder { @NonNull - public abstract Builder setAuthToken(@NonNull String value); + public abstract Builder setToken(@NonNull String value); @NonNull public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index c5fea1c252b..39b289c0834 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -58,4 +58,6 @@ dependencies { androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'junit:junit:4.12' androidTestImplementation "androidx.annotation:annotation:1.0.0" + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' } diff --git a/firebase-installations/src/androidTest/AndroidManifest.xml b/firebase-installations/src/androidTest/AndroidManifest.xml index 43c54f991fd..f9fb55a7b56 100644 --- a/firebase-installations/src/androidTest/AndroidManifest.xml +++ b/firebase-installations/src/androidTest/AndroidManifest.xml @@ -14,7 +14,7 @@ + package="com.google.firebase.installations"> diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java deleted file mode 100644 index c43a6729610..00000000000 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installation/local/FiidCacheTest.java +++ /dev/null @@ -1,110 +0,0 @@ -// 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.installation.local; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.runner.AndroidJUnit4; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.local.FiidCache; -import com.google.firebase.installations.local.FiidCacheEntryValue; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Instrumented tests for {@link FiidCache} */ -@RunWith(AndroidJUnit4.class) -public class FiidCacheTest { - - private FirebaseApp firebaseApp0; - private FirebaseApp firebaseApp1; - private FiidCache cache0; - private FiidCache cache1; - private final String AUTH_TOKEN = "auth_token"; - private final String REFRESH_TOKEN = "refresh_token"; - - private final long TIMESTAMP_IN_SECONDS = 100L; - - @Before - public void setUp() { - FirebaseApp.clearInstancesForTest(); - firebaseApp0 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); - firebaseApp1 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), - "firebase_app_1"); - cache0 = new FiidCache(firebaseApp0); - cache1 = new FiidCache(firebaseApp1); - } - - @After - public void cleanUp() throws Exception { - cache0.clear(); - cache1.clear(); - } - - @Test - public void testReadCacheEntry_Null() { - assertNull(cache0.readCacheEntryValue()); - assertNull(cache1.readCacheEntryValue()); - } - - @Test - public void testUpdateAndReadCacheEntry() throws Exception { - assertTrue( - cache0.insertOrUpdateCacheEntry( - FiidCacheEntryValue.create( - "123456", - FiidCache.CacheStatus.UNREGISTERED, - AUTH_TOKEN, - REFRESH_TOKEN, - TIMESTAMP_IN_SECONDS, - TIMESTAMP_IN_SECONDS))); - FiidCacheEntryValue entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); - assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); - assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.UNREGISTERED); - assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertNull(cache1.readCacheEntryValue()); - - assertTrue( - cache0.insertOrUpdateCacheEntry( - FiidCacheEntryValue.create( - "123456", - FiidCache.CacheStatus.REGISTERED, - AUTH_TOKEN, - REFRESH_TOKEN, - 200L, - TIMESTAMP_IN_SECONDS))); - entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getFirebaseInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getAuthToken()).isEqualTo(AUTH_TOKEN); - assertThat(entryValue.getRefreshToken()).isEqualTo(REFRESH_TOKEN); - assertThat(entryValue.getCacheStatus()).isEqualTo(FiidCache.CacheStatus.REGISTERED); - assertThat(entryValue.getExpiresInSecs()).isEqualTo(TIMESTAMP_IN_SECONDS); - assertThat(entryValue.getTokenCreationEpochInSecs()).isEqualTo(200L); - } -} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java new file mode 100644 index 00000000000..781747f5a5e --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -0,0 +1,220 @@ +// 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.installations; + +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.PersistedFid; +import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.FirebaseInstallationServiceException; +import com.google.firebase.installations.remote.InstallationResponse; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class FirebaseInstallationsInstrumentedTest { + private FirebaseApp firebaseApp; + private ExecutorService executor; + private PersistedFid persistedFid; + @Mock private FirebaseInstallationServiceClient backendClientReturnsOk; + @Mock private FirebaseInstallationServiceClient backendClientReturnsError; + @Mock private PersistedFid persistedFidReturnsError; + @Mock private Utils mockUtils; + @Mock private Clock mockClock; + + @Before + public void setUp() throws FirebaseInstallationServiceException { + MockitoAnnotations.initMocks(this); + FirebaseApp.clearInstancesForTest(); + executor = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new SynchronousQueue<>()); + firebaseApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder() + .setApplicationId(TEST_APP_ID_1) + .setProjectId(TEST_PROJECT_ID) + .setApiKey("api_key") + .build()); + persistedFid = new PersistedFid(firebaseApp); + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder() + .setName("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestampMillis(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .build()); + when(backendClientReturnsError.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenThrow( + new FirebaseInstallationServiceException( + "SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + when(persistedFidReturnsError.insertOrUpdatePersistedFidEntry(any())).thenReturn(false); + when(persistedFidReturnsError.readPersistedFidEntryValue()).thenReturn(null); + when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); + when(mockClock.currentTimeMillis()).thenReturn(TEST_CREATION_TIMESTAMP_1); + } + + @After + public void cleanUp() throws Exception { + persistedFid.clear(); + } + + @Test + public void testGetId_PersistedFidOk_BackendOk() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + // No exception, means success. + assertWithMessage("getId Task fails.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration status doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + } + + @Test + public void testGetId_multipleCalls_sameFIDReturned() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + // No exception, means success. + assertWithMessage("getId Task fails.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + Tasks.await(firebaseInstallations.getId()); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration status doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + } + + @Test + public void testGetId_PersistedFidOk_BackendError() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.getId()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(entryValue.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid doesn't match") + .that(updatedFidEntry.getFirebaseInstallationId()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Registration Fid doesn't match") + .that(updatedFidEntry.getRegistrationStatus()) + .isEqualTo(PersistedFid.RegistrationStatus.REGISTER_ERROR); + } + + @Test + public void testGetId_PersistedFidError_BackendOk() throws InterruptedException { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, + executor, + firebaseApp, + backendClientReturnsOk, + persistedFidReturnsError, + mockUtils); + + // Expect exception + try { + Tasks.await(firebaseInstallations.getId()); + fail(); + } catch (ExecutionException expected) { + Throwable cause = expected.getCause(); + assertWithMessage("Exception class doesn't match") + .that(cause) + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) cause).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); + } + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java new file mode 100644 index 00000000000..97f3989f1de --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +public final class FisAndroidTestConstants { + public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + + public static final String TEST_PROJECT_ID = "777777777777"; + + public static final String TEST_AUTH_TOKEN = "fis.auth.token"; + + public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; + + public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; + public static final String TEST_APP_ID_2 = "1:987654321:android:abcdef"; + + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 1000L; + + public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; + public static final long TEST_CREATION_TIMESTAMP_2 = 2000L; +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java new file mode 100644 index 00000000000..48586508823 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java @@ -0,0 +1,85 @@ +// 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.installations.local; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; +import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +public final class PersistedFidEntrySubject extends Subject { + + // User-defined entry point + public static PersistedFidEntrySubject assertThat( + @NullableDecl PersistedFidEntry persistedFidEntry) { + return assertAbout(PERSISTED_FID_ENTRY_SUBJECT_FACTORY).that(persistedFidEntry); + } + + // Static method for getting the subject factory (for use with assertAbout()) + public static Subject.Factory persistedFidEntry() { + return PERSISTED_FID_ENTRY_SUBJECT_FACTORY; + } + + // Boiler-plate Subject.Factory for EmployeeSubject + private static final Subject.Factory + PERSISTED_FID_ENTRY_SUBJECT_FACTORY = PersistedFidEntrySubject::new; + + private final PersistedFidEntry actual; + + /** + * Constructor for use by subclasses. If you want to create an instance of this class itself, call + * {@link Subject#check(String, PersistedFidEntry ..) check(...)}{@code .that(actual)}. + * + * @param metadata + * @param actual + */ + protected PersistedFidEntrySubject( + FailureMetadata metadata, @NullableDecl PersistedFidEntry actual) { + super(metadata, actual); + this.actual = actual; + } + + // User-defined test assertion + + public void hasFid(String fid) { + check("getFirebaseInstallationId()").that(actual.getFirebaseInstallationId()).isEqualTo(fid); + } + + public void hasAuthToken(String authToken) { + check("getAuthToken()").that(actual.getAuthToken()).isEqualTo(authToken); + } + + public void hasRefreshToken(String refreshToken) { + check("getRefreshToken()").that(actual.getRefreshToken()).isEqualTo(refreshToken); + } + + public void hasCreationTimestamp(long creationTimestamp) { + check("getTokenCreationEpochInSecs()") + .that(actual.getTokenCreationEpochInSecs()) + .isEqualTo(creationTimestamp); + } + + public void hasTokenExpirationTimestamp(long tokenExpirationTimestamp) { + check("getExpiresInSecs()").that(actual.getExpiresInSecs()).isEqualTo(tokenExpirationTimestamp); + } + + public void hasRegistrationStatus(RegistrationStatus registrationStatus) { + check("getRegistrationStatus()") + .that(actual.getRegistrationStatus()) + .isEqualTo(registrationStatus); + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java new file mode 100644 index 00000000000..f6aa77f45de --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java @@ -0,0 +1,120 @@ +// 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.installations.local; + +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; +import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Instrumented tests for {@link PersistedFid} */ +@RunWith(AndroidJUnit4.class) +public class PersistedFidTest { + + private FirebaseApp firebaseApp0; + private FirebaseApp firebaseApp1; + private PersistedFid persistedFid0; + private PersistedFid persistedFid1; + + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + firebaseApp0 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId(TEST_APP_ID_1).build()); + firebaseApp1 = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId(TEST_APP_ID_2).build(), + "firebase_app_1"); + persistedFid0 = new PersistedFid(firebaseApp0); + persistedFid1 = new PersistedFid(firebaseApp1); + } + + @After + public void cleanUp() throws Exception { + persistedFid0.clear(); + persistedFid1.clear(); + } + + @Test + public void testReadPersistedFidEntry_Null() { + assertNull(persistedFid0.readPersistedFidEntryValue()); + assertNull(persistedFid1.readPersistedFidEntryValue()); + } + + @Test + public void testUpdateAndReadPersistedFidEntry_successful() throws Exception { + // Insert Persisted Fid Entry with Unregistered status in Shared Prefs + assertTrue( + persistedFid0.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())); + PersistedFidEntry entryValue = persistedFid0.readPersistedFidEntryValue(); + + // Validate insertion was successful + assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); + assertThat(entryValue).hasRefreshToken(TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); + assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); + assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_1); + + // Update Persisted Fid Entry with Registered status in Shared Prefs + assertTrue( + persistedFid0.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())); + entryValue = persistedFid0.readPersistedFidEntryValue(); + + // Validate update was successful + assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); + assertThat(entryValue).hasRefreshToken(TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.REGISTERED); + assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); + assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_2); + } +} diff --git a/firebase-installations/src/main/AndroidManifest.xml b/firebase-installations/src/main/AndroidManifest.xml index 08b562940aa..4b67e67a5a9 100644 --- a/firebase-installations/src/main/AndroidManifest.xml +++ b/firebase-installations/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + ()), + firebaseApp, + new FirebaseInstallationServiceClient(), + new PersistedFid(firebaseApp), + new Utils()); + } + + FirebaseInstallations( + Clock clock, + Executor executor, + FirebaseApp firebaseApp, + FirebaseInstallationServiceClient serviceClient, + PersistedFid persistedFid, + Utils utils) { + this.clock = clock; this.firebaseApp = firebaseApp; + this.serviceClient = serviceClient; + this.executor = executor; + this.persistedFid = persistedFid; + this.utils = utils; } /** @@ -71,7 +110,9 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { @NonNull @Override public Task getId() { - return Tasks.forResult("fid-is-better-than-iid"); + return Tasks.call(executor, this::getPersistedFid) + .continueWith(orElse(this::createAndPersistNewFid)) + .onSuccessTask(this::registerFidIfNecessary); } /** Returns a auth token(public key) of this Firebase app installation. */ @@ -103,4 +144,108 @@ String getApplicationId() { String getName() { return firebaseApp.getName(); } + + private PersistedFidEntry getPersistedFid() throws FirebaseInstallationsException { + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + if (persistedFidMissingOrInErrorState(persistedFidEntry)) { + throw new FirebaseInstallationsException( + "Failed to get existing fid.", FirebaseInstallationsException.Status.CLIENT_ERROR); + } + return persistedFidEntry; + } + + private static boolean persistedFidMissingOrInErrorState(PersistedFidEntry persistedFidEntry) { + return persistedFidEntry == null + || persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTER_ERROR; + } + + @NonNull + private static Continuation orElse(@NonNull Supplier supplier) { + return t -> { + if (t.isSuccessful()) { + return (T) t.getResult(); + } + return supplier.get(); + }; + } + + private PersistedFidEntry createAndPersistNewFid() throws FirebaseInstallationsException { + String fid = utils.createRandomFid(); + persistFid(fid); + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + return persistedFidEntry; + } + + private void persistFid(String fid) throws FirebaseInstallationsException { + boolean firstUpdateCacheResult = + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.UNREGISTERED) + .build()); + + if (!firstUpdateCacheResult) { + throw new FirebaseInstallationsException( + "Failed to update client side cache.", + FirebaseInstallationsException.Status.CLIENT_ERROR); + } + } + + private Task registerFidIfNecessary(PersistedFidEntry persistedFidEntry) { + String fid = persistedFidEntry.getFirebaseInstallationId(); + + // Check if the fid is unregistered + if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.UNREGISTERED) { + updatePersistedFidWithPendingStatus(fid); + Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); + } + return Tasks.forResult(fid); + } + + private void updatePersistedFidWithPendingStatus(String fid) { + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.PENDING) + .build()); + } + + /** Registers the created Fid with FIS servers and update the shared prefs. */ + private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) + throws FirebaseInstallationsException { + try { + long creationTime = TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); + + InstallationResponse installationResponse = + serviceClient.createFirebaseInstallation( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ persistedFidEntry.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*appId= */ getApplicationId()); + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(installationResponse.getAuthToken().getToken()) + .setRefreshToken(installationResponse.getRefreshToken()) + .setExpiresInSecs( + installationResponse.getAuthToken().getTokenExpirationTimestampMillis()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + + } catch (FirebaseInstallationServiceException exception) { + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) + .build()); + throw new FirebaseInstallationsException( + exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + return null; + } +} + +interface Supplier { + T get() throws Exception; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java new file mode 100644 index 00000000000..fd627a27911 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseException; + +/** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ +public class FirebaseInstallationsException extends FirebaseException { + + // TODO(ankitagj): Improve clear exception handling. + public enum Status { + SDK_INTERNAL_ERROR, + + CLIENT_ERROR + } + + @NonNull private final Status status; + + public FirebaseInstallationsException(@NonNull Status status) { + this.status = status; + } + + public FirebaseInstallationsException(@NonNull String message, @NonNull Status status) { + super(message); + this.status = status; + } + + public FirebaseInstallationsException( + @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { + super(message, cause); + this.status = status; + } + + /** + * Gets the status for the operation that failed. + * + * @return the status for the FirebaseInstallationsException + */ + @NonNull + public Status getStatus() { + return status; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java new file mode 100644 index 00000000000..1ad7efab2f2 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -0,0 +1,86 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.UUID; + +/** Util methods used for {@link FirebaseInstallations} */ +class Utils { + + /** + * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this + * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy + * Instance-IDs + */ + public static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); + + /** + * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND + * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. + */ + public static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); + + /** Length of new-format FIDs as introduced in 2019. */ + public static final int FID_LENGTH = 22; + + /** + * Creates a random FID of valid format without checking if the FID is already in use by any + * Firebase Installation. + * + *

Note: Even though this method does not check with the FIS database if the returned FID is + * already in use, the probability of collision is extremely and negligibly small! + * + * @return random FID value + */ + @NonNull + public String createRandomFid() { + // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. + byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); + uuidBytes[16] = uuidBytes[0]; + uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); + return encodeFidBase64UrlSafe(uuidBytes); + } + + /** + * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded + * String-representation. + * + *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is + * recommended to deliver a byte-array containing at least 16.5 bytes. + * + * @param rawValue FID value to be encoded + * @return (22-character or shorter) String containing the base64-encoded value + */ + private static String encodeFidBase64UrlSafe(byte[] rawValue) { + return new String( + android.util.Base64.encode( + rawValue, + android.util.Base64.URL_SAFE + | android.util.Base64.NO_PADDING + | android.util.Base64.NO_WRAP), + Charset.defaultCharset()) + .substring(0, FID_LENGTH); + } + + private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { + ByteBuffer bb = ByteBuffer.wrap(output); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java deleted file mode 100644 index 3d42ca15bbf..00000000000 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCacheEntryValue.java +++ /dev/null @@ -1,59 +0,0 @@ -// 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.installations.local; - -import androidx.annotation.NonNull; -import com.google.auto.value.AutoValue; - -/** - * This class represents a cache entry value in {@link FiidCache}, which contains a Firebase - * instance id and the cache status of this entry. - */ -@AutoValue -public abstract class FiidCacheEntryValue { - - @NonNull - public abstract String getFirebaseInstallationId(); - - @NonNull - public abstract FiidCache.CacheStatus getCacheStatus(); - - @NonNull - public abstract String getAuthToken(); - - @NonNull - public abstract String getRefreshToken(); - - public abstract long getExpiresInSecs(); - - public abstract long getTokenCreationEpochInSecs(); - - @NonNull - public static FiidCacheEntryValue create( - @NonNull String firebaseInstallationId, - @NonNull FiidCache.CacheStatus cacheStatus, - @NonNull String authToken, - @NonNull String refreshToken, - long tokenCreationEpochInSecs, - long expiresInSecs) { - return new AutoValue_FiidCacheEntryValue( - firebaseInstallationId, - cacheStatus, - authToken, - refreshToken, - expiresInSecs, - tokenCreationEpochInSecs); - } -} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java similarity index 58% rename from firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java rename to firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java index 589c9fb9266..177c94fd0ad 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/FiidCache.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java @@ -19,39 +19,50 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.FirebaseApp; +import java.util.Arrays; +import java.util.List; /** - * A layer that locally caches a few Firebase Installation attributes on top the Firebase - * Installation backend API. + * A layer that locally persists a few Firebase Installation attributes on top the Firebase + * Installation API. */ -public class FiidCache { - // 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. - public enum CacheStatus { - // Cache entry is synced to Firebase backend +public class PersistedFid { + // Registration Status of each persisted fid entry + // NOTE: never change the ordinal of the enum values because the enum values are stored in shared + // prefs as their ordinal numbers. + public enum RegistrationStatus { + /** {@link PersistedFidEntry} is synced to FIS servers */ REGISTERED, - // Cache entry is waiting for Firebase backend response or internal network retry + /** {@link PersistedFidEntry} is not synced with FIS server */ UNREGISTERED, - // Cache entry is in error state when syncing with Firebase backend + /** {@link PersistedFidEntry} is in error state when syncing with FIS server */ REGISTER_ERROR, - // Cache entry is in delete state before syncing with Firebase backend - DELETED + /** {@link PersistedFidEntry} is in pending state when waiting for FIS server response */ + PENDING } - private static final String SHARED_PREFS_NAME = "FiidCache"; + private static final String SHARED_PREFS_NAME = "PersistedFid"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; private static final String REFRESH_TOKEN_KEY = "RefreshToken"; private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; - private static final String CACHE_STATUS_KEY = "Status"; + private static final String PERSISTED_STATUS_KEY = "Status"; + + private static final List FID_PREF_KEYS = + Arrays.asList( + FIREBASE_INSTALLATION_ID_KEY, + AUTH_TOKEN_KEY, + REFRESH_TOKEN_KEY, + TOKEN_CREATION_TIME_IN_SECONDS_KEY, + EXPIRES_IN_SECONDS_KEY, + PERSISTED_STATUS_KEY); private final SharedPreferences prefs; private final String persistenceKey; - public FiidCache(@NonNull FirebaseApp firebaseApp) { + public PersistedFid(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path prefs = @@ -62,30 +73,41 @@ public FiidCache(@NonNull FirebaseApp firebaseApp) { } @Nullable - public synchronized FiidCacheEntryValue readCacheEntryValue() { - String iid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); + public synchronized PersistedFidEntry readPersistedFidEntryValue() { + String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); long tokenCreationTime = prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - if (iid == null || status == -1) { + if (fid == null + || status == -1 + || !(status >= 0 && status < RegistrationStatus.values().length)) { return null; } - return FiidCacheEntryValue.create( - iid, CacheStatus.values()[status], authToken, refreshToken, tokenCreationTime, expiresIn); + return PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .build(); } @NonNull - public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValue entryValue) { + public synchronized boolean insertOrUpdatePersistedFidEntry( + @NonNull PersistedFidEntry entryValue) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), entryValue.getFirebaseInstallationId()); - editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); + editor.putInt( + getSharedPreferencesKey(PERSISTED_STATUS_KEY), + entryValue.getRegistrationStatus().ordinal()); editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); editor.putLong( @@ -98,12 +120,10 @@ public synchronized boolean insertOrUpdateCacheEntry(@NonNull FiidCacheEntryValu @NonNull public synchronized boolean clear() { SharedPreferences.Editor editor = prefs.edit(); - editor.remove(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY)); - editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); - editor.remove(getSharedPreferencesKey(AUTH_TOKEN_KEY)); - editor.remove(getSharedPreferencesKey(REFRESH_TOKEN_KEY)); - editor.remove(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY)); - editor.remove(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY)); + for (String k : FID_PREF_KEYS) { + editor.remove(getSharedPreferencesKey(k)); + } + editor.commit(); return editor.commit(); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java new file mode 100644 index 00000000000..4c8a9a56ec4 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java @@ -0,0 +1,78 @@ +// 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.installations.local; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + +/** + * This class represents a persisted fid entry in {@link PersistedFid}, which contains a few + * Firebase Installation attributes and the persisted status of this entry. + */ +@AutoValue +public abstract class PersistedFidEntry { + + @NonNull + public abstract String getFirebaseInstallationId(); + + @NonNull + public abstract PersistedFid.RegistrationStatus getRegistrationStatus(); + + @Nullable + public abstract String getAuthToken(); + + @Nullable + public abstract String getRefreshToken(); + + public abstract long getExpiresInSecs(); + + public abstract long getTokenCreationEpochInSecs(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an PersistedFidEntry object */ + @NonNull + public static PersistedFidEntry.Builder builder() { + return new AutoValue_PersistedFidEntry.Builder() + .setTokenCreationEpochInSecs(0) + .setExpiresInSecs(0); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setFirebaseInstallationId(@NonNull String value); + + @NonNull + public abstract Builder setRegistrationStatus(@NonNull PersistedFid.RegistrationStatus value); + + @NonNull + public abstract Builder setAuthToken(@Nullable String value); + + @NonNull + public abstract Builder setRefreshToken(@Nullable String value); + + @NonNull + public abstract Builder setExpiresInSecs(long value); + + @NonNull + public abstract Builder setTokenCreationEpochInSecs(long value); + + @NonNull + public abstract PersistedFidEntry build(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 51cf2e1a219..c0105a8607b 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -32,10 +32,10 @@ public class FirebaseInstallationServiceClient { "firebaseinstallations.googleapis.com"; private static final String CREATE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations"; private static final String GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT = - "projects/%s/installations/%s/auth:generate"; + "projects/%s/installations/%s/authTokens:generate"; private static final String DELETE_REQUEST_RESOURCE_NAME_FORMAT = "projects/%s/installations/%s"; private static final String FIREBASE_INSTALLATIONS_API_VERSION = "v1"; - private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_V2"; + private static final String FIREBASE_INSTALLATION_AUTH_VERSION = "FIS_v2"; private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; private static final String ACCEPT_HEADER_KEY = "Accept"; @@ -48,14 +48,20 @@ public class FirebaseInstallationServiceClient { private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + /** + * Creates a FID on the FIS Servers by calling FirebaseInstallations API create method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param appId the identifier of a Firebase application + * @return {@link InstallationResponse} generated from the response body + */ @NonNull public InstallationResponse createFirebaseInstallation( - long projectNumber, - @NonNull String apiKey, - @NonNull String firebaseInstallationId, - @NonNull String appId) + @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) throws FirebaseInstallationServiceException { - String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber); + String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); try { URL url = new URL( @@ -76,9 +82,7 @@ public InstallationResponse createFirebaseInstallation( new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(firebaseInstallationId, appId) - .toString() - .getBytes("UTF-8")); + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); } catch (JSONException e) { throw new IllegalStateException(e); } finally { @@ -91,16 +95,16 @@ public InstallationResponse createFirebaseInstallation( return readCreateResponse(httpsURLConnection); case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } @@ -109,15 +113,26 @@ private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, JSONObject firebaseInstallationData = new JSONObject(); firebaseInstallationData.put("fid", fid); firebaseInstallationData.put("appId", appId); - firebaseInstallationData.put("appVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + firebaseInstallationData.put("authVersion", FIREBASE_INSTALLATION_AUTH_VERSION); return firebaseInstallationData; } + /** + * Deletes a FID on the FIS Servers by calling FirebaseInstallations API delete method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param refreshToken a token used to authenticate FIS requests + */ @NonNull public void deleteFirebaseInstallation( - long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + @NonNull String apiKey, + @NonNull String fid, + @NonNull String projectID, + @NonNull String refreshToken) throws FirebaseInstallationServiceException { - String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = new URL( @@ -131,7 +146,7 @@ public void deleteFirebaseInstallation( HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("DELETE"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); @@ -141,25 +156,37 @@ public void deleteFirebaseInstallation( return; case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } + /** + * Generates a new auth token for a FID on the FIS Servers by calling FirebaseInstallations API + * generateAuthToken method. + * + * @param apiKey API Key that has access to FIS APIs + * @param fid Firebase Installation Identifier + * @param projectID Project Id + * @param refreshToken a token used to authenticate FIS requests + */ @NonNull public InstallationTokenResult generateAuthToken( - long projectNumber, @NonNull String apiKey, @NonNull String fid, @NonNull String refreshToken) + @NonNull String apiKey, + @NonNull String fid, + @NonNull String projectID, + @NonNull String refreshToken) throws FirebaseInstallationServiceException { String resourceName = - String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, fid); + String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = new URL( @@ -173,7 +200,7 @@ public InstallationTokenResult generateAuthToken( HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_V2 " + refreshToken); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); @@ -184,16 +211,16 @@ public InstallationTokenResult generateAuthToken( return readGenerateAuthTokenResponse(httpsURLConnection); case 401: throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Code.UNAUTHORIZED); + UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: throw new FirebaseInstallationServiceException( INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Code.SERVER_ERROR); + FirebaseInstallationServiceException.Status.SERVER_ERROR); } } catch (IOException e) { throw new FirebaseInstallationServiceException( NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Code.NETWORK_ERROR); + FirebaseInstallationServiceException.Status.NETWORK_ERROR); } } // Read the response from the createFirebaseInstallation API. @@ -214,7 +241,7 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws while (reader.hasNext()) { String key = reader.nextName(); if (key.equals("token")) { - installationTokenResult.setAuthToken(reader.nextString()); + installationTokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); } else { @@ -242,7 +269,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("token")) { - builder.setAuthToken(reader.nextString()); + builder.setToken(reader.nextString()); } else if (name.equals("expiresIn")) { builder.setTokenExpirationTimestampMillis(reader.nextLong()); } else { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java index b703b56d539..b9be3727d4d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java @@ -19,8 +19,7 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ public class FirebaseInstallationServiceException extends FirebaseException { - - public enum Code { + public enum Status { SERVER_ERROR, NETWORK_ERROR, @@ -28,30 +27,30 @@ public enum Code { UNAUTHORIZED } - @NonNull private final Code code; + @NonNull private final Status status; - FirebaseInstallationServiceException(@NonNull Code code) { - this.code = code; + public FirebaseInstallationServiceException(@NonNull Status status) { + this.status = status; } - FirebaseInstallationServiceException(@NonNull String message, @NonNull Code code) { + public FirebaseInstallationServiceException(@NonNull String message, @NonNull Status status) { super(message); - this.code = code; + this.status = status; } - FirebaseInstallationServiceException( - @NonNull String message, @NonNull Code code, Throwable cause) { + public FirebaseInstallationServiceException( + @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { super(message, cause); - this.code = code; + this.status = status; } /** - * Gets the status code for the operation that failed. + * Gets the status status for the operation that failed. * - * @return the code for the FirebaseInstallationServiceException + * @return the status for the FirebaseInstallationServiceException */ @NonNull - public Code getCode() { - return code; + public Status getStatus() { + return status; } } diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java index 5a7c505e9b6..f553b7d701a 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsRegistrarTest.java @@ -14,5 +14,42 @@ package com.google.firebase.installations; +import static org.junit.Assert.assertNotNull; + +import androidx.test.core.app.ApplicationProvider; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + /** Tests for {@link FirebaseInstallationsRegistrar}. */ -public class FirebaseInstallationsRegistrarTest {} +@RunWith(RobolectricTestRunner.class) +public class FirebaseInstallationsRegistrarTest { + @Before + public void setUp() { + FirebaseApp.clearInstancesForTest(); + } + + @Test + public void getFirebaseInstallationsInstance() { + FirebaseApp defaultApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); + + FirebaseApp anotherApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), + "firebase_app_1"); + + FirebaseInstallations defaultFirebaseInstallation = FirebaseInstallations.getInstance(); + assertNotNull(defaultFirebaseInstallation); + + FirebaseInstallations anotherFirebaseInstallation = + FirebaseInstallations.getInstance(anotherApp); + assertNotNull(anotherFirebaseInstallation); + } +} From e405108720a871a5d12df63146c505f7bfbf12c3 Mon Sep 17 00:00:00 2001 From: Ankita Jhawar Date: Fri, 13 Sep 2019 11:33:44 -0700 Subject: [PATCH 51/74] Updating api.txt to resolve api-information presubmit check. --- firebase-installations/api.txt | 107 +++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 firebase-installations/api.txt diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt new file mode 100644 index 00000000000..6cc5d9bb863 --- /dev/null +++ b/firebase-installations/api.txt @@ -0,0 +1,107 @@ +// Signature format: 2.0 +package com.google.firebase.installations { + + public class FirebaseInstallations { + method @NonNull public Task delete(); + method @NonNull public Task getAuthToken(boolean); + method @NonNull public Task getId(); + method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(); + method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull FirebaseApp); + } + + public class FirebaseInstallationsException { + ctor public FirebaseInstallationsException(@NonNull com.google.firebase.installations.FirebaseInstallationsException.Status); + ctor public FirebaseInstallationsException(@NonNull String, @NonNull com.google.firebase.installations.FirebaseInstallationsException.Status); + ctor public FirebaseInstallationsException(@NonNull String, @NonNull com.google.firebase.installations.FirebaseInstallationsException.Status, @NonNull Throwable); + method @NonNull public com.google.firebase.installations.FirebaseInstallationsException.Status getStatus(); + } + + public enum FirebaseInstallationsException.Status { + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status CLIENT_ERROR; + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status SDK_INTERNAL_ERROR; + } + +} + +package com.google.firebase.installations.local { + + public class PersistedFid { + ctor public PersistedFid(@NonNull FirebaseApp); + method @NonNull public boolean clear(); + method @NonNull public boolean insertOrUpdatePersistedFidEntry(@NonNull com.google.firebase.installations.local.PersistedFidEntry); + method @Nullable public com.google.firebase.installations.local.PersistedFidEntry readPersistedFidEntryValue(); + } + + public enum PersistedFid.RegistrationStatus { + enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus PENDING; + enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTERED; + enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTER_ERROR; + enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus UNREGISTERED; + } + + public abstract class PersistedFidEntry { + ctor public PersistedFidEntry(); + method @NonNull public static com.google.firebase.installations.local.PersistedFidEntry.Builder builder(); + method @Nullable public abstract String getAuthToken(); + method public abstract long getExpiresInSecs(); + method @NonNull public abstract String getFirebaseInstallationId(); + method @Nullable public abstract String getRefreshToken(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFid.RegistrationStatus getRegistrationStatus(); + method public abstract long getTokenCreationEpochInSecs(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder toBuilder(); + } + + public abstract static class PersistedFidEntry.Builder { + ctor public PersistedFidEntry.Builder(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry build(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setAuthToken(@Nullable String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setExpiresInSecs(long); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setFirebaseInstallationId(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setRefreshToken(@Nullable String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setRegistrationStatus(@NonNull com.google.firebase.installations.local.PersistedFid.RegistrationStatus); + method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setTokenCreationEpochInSecs(long); + } + +} + +package com.google.firebase.installations.remote { + + public class FirebaseInstallationServiceClient { + ctor public FirebaseInstallationServiceClient(); + method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; + method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; + method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; + } + + public class FirebaseInstallationServiceException { + ctor public FirebaseInstallationServiceException(@NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); + ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); + ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status, @NonNull Throwable); + method @NonNull public com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status getStatus(); + } + + public enum FirebaseInstallationServiceException.Status { + enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status NETWORK_ERROR; + enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status SERVER_ERROR; + enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status UNAUTHORIZED; + } + + public abstract class InstallationResponse { + ctor public InstallationResponse(); + method @NonNull public static com.google.firebase.installations.remote.InstallationResponse.Builder builder(); + method @NonNull public abstract InstallationTokenResult getAuthToken(); + method @NonNull public abstract String getName(); + method @NonNull public abstract String getRefreshToken(); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder toBuilder(); + } + + public abstract static class InstallationResponse.Builder { + ctor public InstallationResponse.Builder(); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse build(); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull InstallationTokenResult); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setName(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setRefreshToken(@NonNull String); + } + +} + From 8059184393421ce2da19eb80080ea85539974d04 Mon Sep 17 00:00:00 2001 From: Ankita Jhawar Date: Fri, 13 Sep 2019 11:47:28 -0700 Subject: [PATCH 52/74] Adding api.txt for fisbase-installations-interop --- firebase-installations-interop/api.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 firebase-installations-interop/api.txt diff --git a/firebase-installations-interop/api.txt b/firebase-installations-interop/api.txt new file mode 100644 index 00000000000..8f80b256520 --- /dev/null +++ b/firebase-installations-interop/api.txt @@ -0,0 +1,20 @@ +// Signature format: 2.0 +package com.google.firebase.installations { + + public abstract class InstallationTokenResult { + ctor public InstallationTokenResult(); + method @NonNull public static com.google.firebase.installations.InstallationTokenResult.Builder builder(); + method @NonNull public abstract String getToken(); + method @NonNull public abstract long getTokenExpirationTimestampMillis(); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder toBuilder(); + } + + public abstract static class InstallationTokenResult.Builder { + ctor public InstallationTokenResult.Builder(); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult build(); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setToken(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenExpirationTimestampMillis(@NonNull long); + } + +} + From 7ed78a4e01c28559a85942407995cbb25ba2d10c Mon Sep 17 00:00:00 2001 From: Ankita Jhawar Date: Fri, 13 Sep 2019 14:26:30 -0700 Subject: [PATCH 53/74] Updating subprojects.cfg --- subprojects.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/subprojects.cfg b/subprojects.cfg index 4f9d3178289..69674dd9996 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -15,6 +15,8 @@ firebase-functions firebase-functions:ktx firebase-inappmessaging firebase-inappmessaging-display +firebase-installations-interop +firebase-installations firebase-storage firebase-storage:ktx firebase-storage:test-app From 6a013fa96ecc261cf0ecd3fc3710613a4479fbad Mon Sep 17 00:00:00 2001 From: Ankita Date: Mon, 16 Sep 2019 13:46:07 -0700 Subject: [PATCH 54/74] FIS getAuthToken implementation. (#769) * FIS getAuthToken implementation. * FIS getAuthToken implementation. * 1. Addressing Ciaran's comments 2. Renaming InstallationTokenResult TokenExpirationTimestampMillis field to TokenExpirationInSecs * 1. Addressing Ciaran's comments 2. Renaming InstallationTokenResult TokenExpirationTimestampMillis field to TokenExpirationInSecs * Updated IntDef usage in firebase-installations * Using CountDownLatch to await instead of executor.awaitTermination * Addressing Di's comments. * Addressing Di's comments. * Addressing Ciaran's comments to replace latch with a custom listener. * Adding onSuccess to AwaitListener. * Cleaning up the code as per the ciaran's comments. * Addressing Di's comments. * 1. Handling multiple calls to getAUthToken() 2. Minor code changes as per the offline discussion with Rayo * Fixing api-information presubmit check. * updating api.txt for firbase-installations-interop * Addressing Ciaran's comments. * Fixing check-changed - GoogleJavaFormat error. * Addressing ciaran`s comments * nit fixes to executor : use unblocking queue. --- firebase-installations-interop/api.txt | 4 +- .../FirebaseInstallationsApi.java | 21 +- .../InstallationTokenResult.java | 7 +- firebase-installations/api.txt | 2 +- ...FirebaseInstallationsInstrumentedTest.java | 282 +++++++++++++++++- .../FisAndroidTestConstants.java | 6 + .../firebase/installations/AwaitListener.java | 38 +++ .../installations/FirebaseInstallations.java | 184 +++++++++++- .../installations/local/PersistedFid.java | 50 ++-- .../FirebaseInstallationServiceClient.java | 6 +- 10 files changed, 545 insertions(+), 55 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/AwaitListener.java diff --git a/firebase-installations-interop/api.txt b/firebase-installations-interop/api.txt index 8f80b256520..b2a54de1051 100644 --- a/firebase-installations-interop/api.txt +++ b/firebase-installations-interop/api.txt @@ -5,7 +5,7 @@ package com.google.firebase.installations { ctor public InstallationTokenResult(); method @NonNull public static com.google.firebase.installations.InstallationTokenResult.Builder builder(); method @NonNull public abstract String getToken(); - method @NonNull public abstract long getTokenExpirationTimestampMillis(); + method @NonNull public abstract long getTokenExpirationInSecs(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder toBuilder(); } @@ -13,7 +13,7 @@ package com.google.firebase.installations { ctor public InstallationTokenResult.Builder(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult build(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setToken(@NonNull String); - method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenExpirationTimestampMillis(@NonNull long); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenExpirationInSecs(long); } } diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java index 6947d7ec037..7ecdb6a9961 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -14,7 +14,11 @@ package com.google.firebase.installations; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; import com.google.android.gms.tasks.Task; +import java.lang.annotation.Retention; /** * This is an interface of {@code FirebaseInstallations} that is only exposed to 2p via component @@ -24,6 +28,21 @@ */ public interface FirebaseInstallationsApi { + /** Specifies the options to get a FIS AuthToken. */ + @IntDef({DO_NOT_FORCE_REFRESH, FORCE_REFRESH}) + @Retention(SOURCE) + @interface AuthTokenOption {} + /** + * AuthToken is not refreshed until requested by the developer or if one doesn't exist, is expired + * or about to expire. + */ + int DO_NOT_FORCE_REFRESH = 0; + /** + * AuthToken is forcefully refreshed on calling the {@link + * FirebaseInstallationsApi#getAuthToken(int)}. + */ + int FORCE_REFRESH = 1; + /** * Async function that returns a globally unique identifier of this Firebase app installation. * This is a url-safe base64 string of a 128-bit integer. @@ -31,7 +50,7 @@ public interface FirebaseInstallationsApi { Task getId(); /** Async function that returns a auth token(public key) of this Firebase app installation. */ - Task getAuthToken(boolean forceRefresh); + Task getAuthToken(@AuthTokenOption int authTokenOption); /** * Async function that deletes this Firebase app installation from Firebase backend. This call diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index 5cd761299eb..94266bcc9f2 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -25,11 +25,10 @@ public abstract class InstallationTokenResult { @NonNull public abstract String getToken(); /** - * The amount of time, in milliseconds, before the auth-token expires for this Firebase - * Installation. + * The amount of time, in seconds, before the auth-token expires for this Firebase Installation. */ @NonNull - public abstract long getTokenExpirationTimestampMillis(); + public abstract long getTokenExpirationInSecs(); @NonNull public abstract Builder toBuilder(); @@ -46,7 +45,7 @@ public abstract static class Builder { public abstract Builder setToken(@NonNull String value); @NonNull - public abstract Builder setTokenExpirationTimestampMillis(@NonNull long value); + public abstract Builder setTokenExpirationInSecs(long value); @NonNull public abstract InstallationTokenResult build(); diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 6cc5d9bb863..fed20da199e 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -3,7 +3,7 @@ package com.google.firebase.installations { public class FirebaseInstallations { method @NonNull public Task delete(); - method @NonNull public Task getAuthToken(boolean); + method @NonNull public Task getAuthToken(int); method @NonNull public Task getId(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull FirebaseApp); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 781747f5a5e..502bef1b66a 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -15,21 +15,31 @@ package com.google.firebase.installations; import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_3; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; import com.google.android.gms.common.util.Clock; +import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -40,7 +50,7 @@ import com.google.firebase.installations.remote.InstallationResponse; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -49,6 +59,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; +import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -68,19 +79,60 @@ public class FirebaseInstallationsInstrumentedTest { @Mock private PersistedFid persistedFidReturnsError; @Mock private Utils mockUtils; @Mock private Clock mockClock; + @Mock private PersistedFid mockPersistedFid; + + private static final PersistedFidEntry REGISTERED_FID_ENTRY = + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .build(); + + private static final PersistedFidEntry EXPIRED_AUTH_TOKEN_ENTRY = + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) + .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .build(); + + private static final PersistedFidEntry UNREGISTERED_FID_ENTRY = + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken("") + .setRefreshToken("") + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(0) + .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) + .build(); + + private static final PersistedFidEntry UPDATED_AUTH_TOKEN_FID_ENTRY = + PersistedFidEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN_2) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .build(); @Before public void setUp() throws FirebaseInstallationServiceException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); - executor = new ThreadPoolExecutor(0, 2, 10L, TimeUnit.SECONDS, new SynchronousQueue<>()); + executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder() .setApplicationId(TEST_APP_ID_1) .setProjectId(TEST_PROJECT_ID) - .setApiKey("api_key") + .setApiKey(TEST_API_KEY) .build()); persistedFid = new PersistedFid(firebaseApp); when(backendClientReturnsOk.createFirebaseInstallation( @@ -92,9 +144,16 @@ public void setUp() throws FirebaseInstallationServiceException { .setAuthToken( InstallationTokenResult.builder() .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationTimestampMillis(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) .build()) .build()); + when(backendClientReturnsOk.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN_2) + .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()); when(backendClientReturnsError.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenThrow( @@ -206,15 +265,222 @@ public void testGetId_PersistedFidError_BackendOk() throws InterruptedException // Expect exception try { Tasks.await(firebaseInstallations.getId()); - fail(); + fail("Could not update local storage."); } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); assertWithMessage("Exception class doesn't match") - .that(cause) + .that(expected) + .hasCauseThat() .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) cause).getStatus()) + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); } } + + @Test + public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Auth Token doesn't match") + .that(entryValue.getAuthToken()) + .isEqualTo(TEST_AUTH_TOKEN); + } + + @Test + public void testGetAuthToken_PersistedFidError_failure() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, + executor, + firebaseApp, + backendClientReturnsOk, + persistedFidReturnsError, + mockUtils); + + // Expect exception + try { + Tasks.await( + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + fail("Could not update local storage."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + } + + @Test + public void testGetAuthToken_fidExists_successful() throws Exception { + when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + + InstallationTokenResult installationTokenResult = + Tasks.await( + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + } + + @Test + public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { + when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(EXPIRED_AUTH_TOKEN_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + + InstallationTokenResult installationTokenResult = + Tasks.await( + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + } + + @Test + public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { + // Using mockPersistedFid to ensure the order of returning persistedFidEntry. This test + // validates that getAuthToken calls getId to ensure FID registration and returns a valid auth + // token. + when(mockPersistedFid.readPersistedFidEntryValue()) + .thenReturn(UNREGISTERED_FID_ENTRY, REGISTERED_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + + InstallationTokenResult installationTokenResult = + Tasks.await( + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + } + + @Test + public void testGetAuthToken_serverError_failure() throws Exception { + when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); + when(backendClientReturnsError.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenThrow( + new FirebaseInstallationServiceException( + "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, + executor, + firebaseApp, + backendClientReturnsError, + mockPersistedFid, + mockUtils); + + // Expect exception + try { + Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); + fail("getAuthToken() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + } + + @Test + public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() + throws Exception { + // Using mockPersistedFid to ensure the order of returning persistedFidEntry to 2 tasks + // triggered simultaneously. Task2 waits for Task1 to complete. On Task1 completion, task2 reads + // the UPDATED_AUTH_TOKEN_FID_ENTRY by Task1 on execution. + when(mockPersistedFid.readPersistedFidEntryValue()) + .thenReturn( + EXPIRED_AUTH_TOKEN_ENTRY, + EXPIRED_AUTH_TOKEN_ENTRY, + EXPIRED_AUTH_TOKEN_ENTRY, + UPDATED_AUTH_TOKEN_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + + // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option + Task task1 = + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + Task task2 = + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + + Tasks.await(Tasks.whenAllComplete(task1, task2)); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + verify(backendClientReturnsOk, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { + when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); + // Use a mock ServiceClient for network calls with delay(1000ms) to ensure first task is not + // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() + // and verify one task waits for another task to complete. + + doAnswer( + AdditionalAnswers.answersWithDelay( + 1000, + (unused) -> + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN_3) + .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())) + .doAnswer( + AdditionalAnswers.answersWithDelay( + 1000, + (unused) -> + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN_4) + .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build())) + .when(backendClientReturnsOk) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + + // Call getAuthToken multiple times with FORCE_REFRESH option. + Task task1 = + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH); + Task task2 = + firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH); + Tasks.await(Tasks.whenAllComplete(task1, task2)); + + // As we cannot ensure which task got executed first, verifying with both expected values + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_3); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_4); + verify(backendClientReturnsOk, times(2)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 97f3989f1de..e77a2b1155b 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -20,6 +20,11 @@ public final class FisAndroidTestConstants { public static final String TEST_PROJECT_ID = "777777777777"; public static final String TEST_AUTH_TOKEN = "fis.auth.token"; + public static final String TEST_AUTH_TOKEN_2 = "fis.auth.token2"; + public static final String TEST_AUTH_TOKEN_3 = "fis.auth.token3"; + public static final String TEST_AUTH_TOKEN_4 = "fis.auth.token4"; + + public static final String TEST_API_KEY = "apiKey"; public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; @@ -27,6 +32,7 @@ public final class FisAndroidTestConstants { public static final String TEST_APP_ID_2 = "1:987654321:android:abcdef"; public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 1000L; + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP_2 = 2000L; public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2000L; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/AwaitListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/AwaitListener.java new file mode 100644 index 00000000000..c7ecab4396a --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/AwaitListener.java @@ -0,0 +1,38 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +final class AwaitListener implements OnCompleteListener { + private final CountDownLatch latch = new CountDownLatch(1); + + public void onSuccess() { + latch.countDown(); + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return latch.await(timeout, unit); + } + + @Override + public void onComplete(@NonNull Task task) { + latch.countDown(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 4af7d5578a0..4072bdc1c06 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -29,8 +29,8 @@ import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import com.google.firebase.installations.remote.InstallationResponse; -import java.util.concurrent.Executor; -import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -50,15 +50,18 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final FirebaseApp firebaseApp; private final FirebaseInstallationServiceClient serviceClient; private final PersistedFid persistedFid; - private final Executor executor; + private final ExecutorService executor; private final Clock clock; private final Utils utils; + private static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = 3600L; // 1 hour + private static final long AWAIT_TIMEOUT_IN_SECS = 10L; + /** package private constructor. */ FirebaseInstallations(FirebaseApp firebaseApp) { this( DefaultClock.getInstance(), - new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new SynchronousQueue<>()), + new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), firebaseApp, new FirebaseInstallationServiceClient(), new PersistedFid(firebaseApp), @@ -67,7 +70,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { FirebaseInstallations( Clock clock, - Executor executor, + ExecutorService executor, FirebaseApp firebaseApp, FirebaseInstallationServiceClient serviceClient, PersistedFid persistedFid, @@ -110,16 +113,39 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { @NonNull @Override public Task getId() { + return getId(null); + } + + /** + * Returns a globally unique identifier of this Firebase app installation.Also, updates the {@link + * AwaitListener} when the FID registration is complete. + */ + private Task getId(AwaitListener awaitListener) { return Tasks.call(executor, this::getPersistedFid) .continueWith(orElse(this::createAndPersistNewFid)) - .onSuccessTask(this::registerFidIfNecessary); + .onSuccessTask( + persistedFidEntry -> registerFidIfNecessary(persistedFidEntry, awaitListener)); } - /** Returns a auth token(public key) of this Firebase app installation. */ + /** + * Returns a valid authentication token for the Firebase installation. Generates a new token if + * one doesn't exist, is expired or about to expire. + * + *

Should only be called if the Firebase Installation is registered. + * + * @param authTokenOption Options to get FIS Auth Token either by force refreshing or not. Accepts + * {@link AuthTokenOption} values. Default value of AuthTokenOption = DO_NOT_FORCE_REFRESH. + */ @NonNull @Override - public Task getAuthToken(boolean forceRefresh) { - return Tasks.forResult(InstallationTokenResult.builder().build()); + public synchronized Task getAuthToken( + @AuthTokenOption int authTokenOption) { + AwaitListener awaitListener = new AwaitListener(); + return getId(awaitListener) + .continueWith( + executor, + awaitFidRegistration( + () -> refreshAuthTokenIfNecessary(authTokenOption), awaitListener)); } /** @@ -145,6 +171,12 @@ String getName() { return firebaseApp.getName(); } + /** + * Returns the {@link PersistedFidEntry} from shared prefs. + * + * @throws {@link FirebaseInstallationsException} when shared pref is empty or {@link + * PersistedFidEntry} is in error state. + */ private PersistedFidEntry getPersistedFid() throws FirebaseInstallationsException { PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); if (persistedFidMissingOrInErrorState(persistedFidEntry)) { @@ -169,6 +201,17 @@ private static Continuation orElse(@NonNull Supplier supplier) { }; } + @NonNull + private static Continuation awaitFidRegistration( + @NonNull Supplier supplier, AwaitListener listener) { + return t -> { + // Waiting for Task that registers FID on the FIS Servers + listener.await(AWAIT_TIMEOUT_IN_SECS, TimeUnit.SECONDS); + return supplier.get(); + }; + } + + /** Creates a random FID and persists it in the shared prefs with UNREGISTERED status. */ private PersistedFidEntry createAndPersistNewFid() throws FirebaseInstallationsException { String fid = utils.createRandomFid(); persistFid(fid); @@ -191,17 +234,45 @@ private void persistFid(String fid) throws FirebaseInstallationsException { } } - private Task registerFidIfNecessary(PersistedFidEntry persistedFidEntry) { + /** + * Registers the FID with FIS servers if FID is in UNREGISTERED state. + * + *

Updates FID registration status to PENDING to avoid multiple network calls to FIS Servers. + */ + private Task registerFidIfNecessary( + PersistedFidEntry persistedFidEntry, AwaitListener listener) { String fid = persistedFidEntry.getFirebaseInstallationId(); // Check if the fid is unregistered if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.UNREGISTERED) { updatePersistedFidWithPendingStatus(fid); - Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); + executeFidRegistration(persistedFidEntry, listener); + } else { + updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener); } + return Tasks.forResult(fid); } + private void updateAwaitListenerIfRegisteredFid( + PersistedFidEntry persistedFidEntry, AwaitListener listener) { + if (listener != null + && persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED) { + listener.onSuccess(); + } + } + + /** + * Registers the FID with FIS servers in a background thread and updates the listener on + * completion. + */ + private void executeFidRegistration(PersistedFidEntry persistedFidEntry, AwaitListener listener) { + Task task = Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); + if (listener != null) { + task.addOnCompleteListener(listener); + } + } + private void updatePersistedFidWithPendingStatus(String fid) { persistedFid.insertOrUpdatePersistedFidEntry( PersistedFidEntry.builder() @@ -214,7 +285,7 @@ private void updatePersistedFidWithPendingStatus(String fid) { private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) throws FirebaseInstallationsException { try { - long creationTime = TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); + long creationTime = currentTimeInSecs(); InstallationResponse installationResponse = serviceClient.createFirebaseInstallation( @@ -228,8 +299,7 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) .setRegistrationStatus(RegistrationStatus.REGISTERED) .setAuthToken(installationResponse.getAuthToken().getToken()) .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs( - installationResponse.getAuthToken().getTokenExpirationTimestampMillis()) + .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationInSecs()) .setTokenCreationEpochInSecs(creationTime) .build()); @@ -244,6 +314,92 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) } return null; } + + private InstallationTokenResult refreshAuthTokenIfNecessary(int authTokenOption) + throws FirebaseInstallationsException { + + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + + if (!isPersistedFidRegistered(persistedFidEntry)) { + throw new FirebaseInstallationsException( + "Firebase Installation is not registered.", + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + + switch (authTokenOption) { + case FORCE_REFRESH: + return fetchAuthTokenFromServer(persistedFidEntry); + case DO_NOT_FORCE_REFRESH: + return getValidAuthToken(persistedFidEntry); + default: + throw new FirebaseInstallationsException( + "Incorrect refreshAuthTokenOption.", + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + } + + /** + * Returns a {@link InstallationTokenResult} created from the {@link PersistedFidEntry} if the + * auth token is valid else generates a new auth token by calling the FIS servers. + */ + private InstallationTokenResult getValidAuthToken(PersistedFidEntry persistedFidEntry) + throws FirebaseInstallationsException { + + return isAuthTokenExpired(persistedFidEntry) + ? fetchAuthTokenFromServer(persistedFidEntry) + : InstallationTokenResult.builder() + .setToken(persistedFidEntry.getAuthToken()) + .setTokenExpirationInSecs(persistedFidEntry.getExpiresInSecs()) + .build(); + } + + private boolean isPersistedFidRegistered(PersistedFidEntry persistedFidEntry) { + return persistedFidEntry != null + && persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED; + } + + /** Calls the FIS servers to generate an auth token for this Firebase installation. */ + private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persistedFidEntry) + throws FirebaseInstallationsException { + try { + long creationTime = currentTimeInSecs(); + InstallationTokenResult tokenResult = + serviceClient.generateAuthToken( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ persistedFidEntry.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*refreshToken= */ persistedFidEntry.getRefreshToken()); + + persistedFid.insertOrUpdatePersistedFidEntry( + PersistedFidEntry.builder() + .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(tokenResult.getToken()) + .setRefreshToken(persistedFidEntry.getRefreshToken()) + .setExpiresInSecs(tokenResult.getTokenExpirationInSecs()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + + return tokenResult; + } catch (FirebaseInstallationServiceException exception) { + throw new FirebaseInstallationsException( + "Failed to generate auth token for a Firebase Installation.", + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + } + + /** + * Checks if the FIS Auth token is expired or going to expire in next 1 hour + * (AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS). + */ + private boolean isAuthTokenExpired(PersistedFidEntry persistedFidEntry) { + return (persistedFidEntry.getTokenCreationEpochInSecs() + persistedFidEntry.getExpiresInSecs() + > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS); + } + + private long currentTimeInSecs() { + return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); + } } interface Supplier { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java index 177c94fd0ad..1da104fa350 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java @@ -73,7 +73,7 @@ public PersistedFid(@NonNull FirebaseApp firebaseApp) { } @Nullable - public synchronized PersistedFidEntry readPersistedFidEntryValue() { + public PersistedFidEntry readPersistedFidEntryValue() { String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); @@ -99,32 +99,36 @@ public synchronized PersistedFidEntry readPersistedFidEntryValue() { } @NonNull - public synchronized boolean insertOrUpdatePersistedFidEntry( - @NonNull PersistedFidEntry entryValue) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString( - getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), - entryValue.getFirebaseInstallationId()); - editor.putInt( - getSharedPreferencesKey(PERSISTED_STATUS_KEY), - entryValue.getRegistrationStatus().ordinal()); - editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); - editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); - editor.putLong( - getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), - entryValue.getTokenCreationEpochInSecs()); - editor.putLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); - return editor.commit(); + public boolean insertOrUpdatePersistedFidEntry(@NonNull PersistedFidEntry entryValue) { + synchronized (prefs) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString( + getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), + entryValue.getFirebaseInstallationId()); + editor.putInt( + getSharedPreferencesKey(PERSISTED_STATUS_KEY), + entryValue.getRegistrationStatus().ordinal()); + editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); + editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); + editor.putLong( + getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), + entryValue.getTokenCreationEpochInSecs()); + editor.putLong( + getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); + return editor.commit(); + } } @NonNull - public synchronized boolean clear() { - SharedPreferences.Editor editor = prefs.edit(); - for (String k : FID_PREF_KEYS) { - editor.remove(getSharedPreferencesKey(k)); + public boolean clear() { + synchronized (prefs) { + SharedPreferences.Editor editor = prefs.edit(); + for (String k : FID_PREF_KEYS) { + editor.remove(getSharedPreferencesKey(k)); + } + editor.commit(); + return editor.commit(); } - editor.commit(); - return editor.commit(); } private String getSharedPreferencesKey(String key) { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index c0105a8607b..53754fc5806 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HttpsURLConnection; import org.json.JSONException; @@ -243,7 +244,8 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws if (key.equals("token")) { installationTokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { - installationTokenResult.setTokenExpirationTimestampMillis(reader.nextLong()); + installationTokenResult.setTokenExpirationInSecs( + TimeUnit.MILLISECONDS.toSeconds(reader.nextLong())); } else { reader.skipValue(); } @@ -271,7 +273,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection if (name.equals("token")) { builder.setToken(reader.nextString()); } else if (name.equals("expiresIn")) { - builder.setTokenExpirationTimestampMillis(reader.nextLong()); + builder.setTokenExpirationInSecs(TimeUnit.MILLISECONDS.toSeconds(reader.nextLong())); } else { reader.skipValue(); } From 5b0acbf7645c9f2bf75b79aabddc243cacf98365 Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 18 Sep 2019 11:30:49 -0700 Subject: [PATCH 55/74] FID delete() implementation. (#813) * FID delete() implementation. * Addressing ciaran's comments. * Refractoring the code to be inline. --- ...FirebaseInstallationsInstrumentedTest.java | 86 +++++++++++++++++++ .../installations/FirebaseInstallations.java | 30 ++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 502bef1b66a..f28b63517a4 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -32,6 +32,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -163,6 +166,16 @@ public void setUp() throws FirebaseInstallationServiceException { when(persistedFidReturnsError.readPersistedFidEntryValue()).thenReturn(null); when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); when(mockClock.currentTimeMillis()).thenReturn(TEST_CREATION_TIMESTAMP_1); + // Mocks success on FIS deletion + doNothing() + .when(backendClientReturnsOk) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + // Mocks server error on FIS deletion + doThrow( + new FirebaseInstallationServiceException( + "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)) + .when(backendClientReturnsError) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @After @@ -483,4 +496,77 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th verify(backendClientReturnsOk, times(2)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } + + @Test + public void testDelete_registeredFID_successful() throws Exception { + // Update local storage with a registered fid entry + persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.delete()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + verify(backendClientReturnsOk, times(1)) + .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testDelete_unregisteredFID_successful() throws Exception { + // Update local storage with a unregistered fid entry + persistedFid.insertOrUpdatePersistedFidEntry(UNREGISTERED_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.delete()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + verify(backendClientReturnsOk, never()) + .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testDelete_emptyPersistedFidEntry_successful() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.delete()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + verify(backendClientReturnsOk, never()) + .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testDelete_serverError_failure() throws Exception { + // Update local storage with a registered fid entry + persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + mockClock, executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + + // Expect exception + try { + Tasks.await(firebaseInstallations.delete()); + fail("delete() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertWithMessage("Persisted Fid Entry doesn't match") + .that(entryValue) + .isEqualTo(REGISTERED_FID_ENTRY); + } + } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 4072bdc1c06..416af91d2ba 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -156,7 +156,7 @@ public synchronized Task getAuthToken( @NonNull @Override public Task delete() { - return Tasks.forResult(null); + return Tasks.call(executor, this::deleteFirebaseInstallationId); } /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ @@ -400,6 +400,34 @@ private boolean isAuthTokenExpired(PersistedFidEntry persistedFidEntry) { private long currentTimeInSecs() { return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); } + + /** + * Deletes the firebase installation id of the {@link FirebaseApp} from FIS servers and local + * storage. + */ + private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException { + + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + + if (isPersistedFidRegistered(persistedFidEntry)) { + // Call the FIS servers to delete this firebase installation id. + try { + serviceClient.deleteFirebaseInstallation( + firebaseApp.getOptions().getApiKey(), + persistedFidEntry.getFirebaseInstallationId(), + firebaseApp.getOptions().getProjectId(), + persistedFidEntry.getRefreshToken()); + + } catch (FirebaseInstallationServiceException exception) { + throw new FirebaseInstallationsException( + "Failed to delete a Firebase Installation.", + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + } + + persistedFid.clear(); + return null; + } } interface Supplier { From aa73d957b9f7b8ce28d1c23f23204ed0ddc65d5c Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 18 Sep 2019 15:27:42 -0700 Subject: [PATCH 56/74] Adding headers for Chemist to check API key restriction. (#821) --- firebase-installations/api.txt | 2 +- .../installations/FirebaseInstallations.java | 2 +- .../FirebaseInstallationServiceClient.java | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index fed20da199e..1b73247dcfa 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -67,7 +67,7 @@ package com.google.firebase.installations.local { package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { - ctor public FirebaseInstallationServiceClient(); + ctor public FirebaseInstallationServiceClient(@NonNull Context); method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 416af91d2ba..b2390ec8f06 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -63,7 +63,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { DefaultClock.getInstance(), new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), firebaseApp, - new FirebaseInstallationServiceClient(), + new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), new PersistedFid(firebaseApp), new Utils()); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 53754fc5806..6dc93ab03ce 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -14,8 +14,15 @@ package com.google.firebase.installations.remote; +import static android.content.ContentValues.TAG; + +import android.content.Context; +import android.content.pm.PackageManager; import android.util.JsonReader; +import android.util.Log; import androidx.annotation.NonNull; +import com.google.android.gms.common.util.AndroidUtilsLight; +import com.google.android.gms.common.util.Hex; import com.google.firebase.installations.InstallationTokenResult; import java.io.IOException; import java.io.InputStreamReader; @@ -49,6 +56,15 @@ public class FirebaseInstallationServiceClient { private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; + private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; + + private final Context context; + + public FirebaseInstallationServiceClient(@NonNull Context context) { + this.context = context; + } + /** * Creates a FID on the FIS Servers by calling FirebaseInstallations API create method. * @@ -79,6 +95,10 @@ public InstallationResponse createFirebaseInstallation( httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { @@ -282,4 +302,23 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection return builder.build(); } + + /** Gets the Android package's SHA-1 fingerprint. */ + private String getFingerprintHashForPackage() { + byte[] hash; + + try { + hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName()); + + if (hash == null) { + Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName()); + return null; + } else { + return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "No such package: " + context.getPackageName(), e); + return null; + } + } } From ac24fae6aa420f882d2e89fe398d803f2dce606e Mon Sep 17 00:00:00 2001 From: Ankita Date: Thu, 19 Sep 2019 14:28:25 -0700 Subject: [PATCH 57/74] Enabling FIS network call timeout. (#827) * Enabling FIS network call timeout. * Addressing Di's comments --- .../remote/FirebaseInstallationServiceClient.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 6dc93ab03ce..51cc1869fae 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -59,6 +59,8 @@ public class FirebaseInstallationServiceClient { private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; + private static final int NETWORK_TIMEOUT_MILLIS = 10000; + private final Context context; public FirebaseInstallationServiceClient(@NonNull Context context) { @@ -90,6 +92,8 @@ public InstallationResponse createFirebaseInstallation( apiKey)); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("POST"); httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); @@ -165,6 +169,8 @@ public void deleteFirebaseInstallation( apiKey)); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("DELETE"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); @@ -219,6 +225,8 @@ public InstallationTokenResult generateAuthToken( apiKey)); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); httpsURLConnection.setDoOutput(true); httpsURLConnection.setRequestMethod("POST"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); From 12ae11d83df03e8e518c01c0666264b13f533040 Mon Sep 17 00:00:00 2001 From: Ankita Date: Thu, 10 Oct 2019 13:51:30 -0700 Subject: [PATCH 58/74] Simplifying FirebaseInstallations class by adding listeners. (#847) * Simplifying FirebaseInstallations class by adding listeners. * Addressing ciaran's comments to return same token if multiple getAuthToken() calls are triggered simultaneously. * Cleaning doRegistration method. * Fixing FISClient to correctly parse expiration timestamp. (#848) * Updating getAuthToken to return creation timestamp (#884) * Propagating the exceptions to the clients. (#856) --- firebase-installations-interop/api.txt | 6 +- .../InstallationTokenResult.java | 13 +- firebase-installations/api.txt | 10 +- .../firebase-installations.gradle | 2 + ...FirebaseInstallationsInstrumentedTest.java | 316 ++++++++++------- .../FisAndroidTestConstants.java | 26 +- .../installations/local/PersistedFidTest.java | 6 +- .../installations/FirebaseInstallations.java | 320 +++++++----------- .../installations/GetAuthTokenListener.java | 56 +++ .../firebase/installations/GetIdListener.java | 44 +++ .../firebase/installations/StateListener.java | 31 ++ .../google/firebase/installations/Utils.java | 24 ++ .../installations/local/PersistedFid.java | 71 ++-- .../local/PersistedFidEntry.java | 19 +- .../FirebaseInstallationServiceClient.java | 30 +- ...FirebaseInstallationServiceClientTest.java | 55 +++ 16 files changed, 668 insertions(+), 361 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java diff --git a/firebase-installations-interop/api.txt b/firebase-installations-interop/api.txt index b2a54de1051..99b8e8affa4 100644 --- a/firebase-installations-interop/api.txt +++ b/firebase-installations-interop/api.txt @@ -5,7 +5,8 @@ package com.google.firebase.installations { ctor public InstallationTokenResult(); method @NonNull public static com.google.firebase.installations.InstallationTokenResult.Builder builder(); method @NonNull public abstract String getToken(); - method @NonNull public abstract long getTokenExpirationInSecs(); + method @NonNull public abstract long getTokenCreationTimestamp(); + method @NonNull public abstract long getTokenExpirationTimestamp(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder toBuilder(); } @@ -13,7 +14,8 @@ package com.google.firebase.installations { ctor public InstallationTokenResult.Builder(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult build(); method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setToken(@NonNull String); - method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenExpirationInSecs(long); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenCreationTimestamp(long); + method @NonNull public abstract com.google.firebase.installations.InstallationTokenResult.Builder setTokenExpirationTimestamp(long); } } diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java index 94266bcc9f2..3dbc02c3e3e 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/InstallationTokenResult.java @@ -28,7 +28,13 @@ public abstract class InstallationTokenResult { * The amount of time, in seconds, before the auth-token expires for this Firebase Installation. */ @NonNull - public abstract long getTokenExpirationInSecs(); + public abstract long getTokenExpirationTimestamp(); + + /** + * The amount of time, in seconds, when the auth-token was created for this Firebase Installation. + */ + @NonNull + public abstract long getTokenCreationTimestamp(); @NonNull public abstract Builder toBuilder(); @@ -45,7 +51,10 @@ public abstract static class Builder { public abstract Builder setToken(@NonNull String value); @NonNull - public abstract Builder setTokenExpirationInSecs(long value); + public abstract Builder setTokenExpirationTimestamp(long value); + + @NonNull + public abstract Builder setTokenCreationTimestamp(long value); @NonNull public abstract InstallationTokenResult build(); diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 1b73247dcfa..1514498e651 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -29,11 +29,11 @@ package com.google.firebase.installations.local { ctor public PersistedFid(@NonNull FirebaseApp); method @NonNull public boolean clear(); method @NonNull public boolean insertOrUpdatePersistedFidEntry(@NonNull com.google.firebase.installations.local.PersistedFidEntry); - method @Nullable public com.google.firebase.installations.local.PersistedFidEntry readPersistedFidEntryValue(); + method @NonNull public com.google.firebase.installations.local.PersistedFidEntry readPersistedFidEntryValue(); } public enum PersistedFid.RegistrationStatus { - enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus PENDING; + enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus NOT_GENERATED; enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTERED; enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTER_ERROR; enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus UNREGISTERED; @@ -44,10 +44,14 @@ package com.google.firebase.installations.local { method @NonNull public static com.google.firebase.installations.local.PersistedFidEntry.Builder builder(); method @Nullable public abstract String getAuthToken(); method public abstract long getExpiresInSecs(); - method @NonNull public abstract String getFirebaseInstallationId(); + method @Nullable public abstract String getFirebaseInstallationId(); method @Nullable public abstract String getRefreshToken(); method @NonNull public abstract com.google.firebase.installations.local.PersistedFid.RegistrationStatus getRegistrationStatus(); method public abstract long getTokenCreationEpochInSecs(); + method public boolean isErrored(); + method public boolean isNotGenerated(); + method public boolean isRegistered(); + method public boolean isUnregistered(); method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder toBuilder(); } diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 39b289c0834..0ef070e8b6d 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -52,6 +52,8 @@ dependencies { testImplementation 'androidx.test:core:1.2.0' testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" + testImplementation "com.google.truth:truth:$googleTruthVersion" + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index f28b63517a4..bd6ef7eb292 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -15,6 +15,7 @@ package com.google.firebase.installations; import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_FID_ENTRY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; @@ -24,10 +25,13 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_TOKEN_RESULT; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; +import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -41,16 +45,15 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; -import com.google.android.gms.common.util.Clock; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.installations.local.PersistedFid; +import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; import com.google.firebase.installations.local.PersistedFidEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.FirebaseInstallationServiceException; -import com.google.firebase.installations.remote.InstallationResponse; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -81,8 +84,8 @@ public class FirebaseInstallationsInstrumentedTest { @Mock private FirebaseInstallationServiceClient backendClientReturnsError; @Mock private PersistedFid persistedFidReturnsError; @Mock private Utils mockUtils; - @Mock private Clock mockClock; @Mock private PersistedFid mockPersistedFid; + @Mock private FirebaseInstallationServiceClient mockClient; private static final PersistedFidEntry REGISTERED_FID_ENTRY = PersistedFidEntry.builder() @@ -99,7 +102,7 @@ public class FirebaseInstallationsInstrumentedTest { .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) .build(); @@ -109,12 +112,12 @@ public class FirebaseInstallationsInstrumentedTest { .setFirebaseInstallationId(TEST_FID_1) .setAuthToken("") .setRefreshToken("") - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) .setExpiresInSecs(0) .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) .build(); - private static final PersistedFidEntry UPDATED_AUTH_TOKEN_FID_ENTRY = + private static final PersistedFidEntry UPDATED_AUTH_TOKEN_ENTRY = PersistedFidEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN_2) @@ -138,34 +141,28 @@ public void setUp() throws FirebaseInstallationServiceException { .setApiKey(TEST_API_KEY) .build()); persistedFid = new PersistedFid(firebaseApp); + when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) - .thenReturn( - InstallationResponse.builder() - .setName("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setAuthToken( - InstallationTokenResult.builder() - .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()) - .build()); + .thenReturn(TEST_INSTALLATION_RESPONSE); + // Mocks successful auth token generation when(backendClientReturnsOk.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenReturn( - InstallationTokenResult.builder() - .setToken(TEST_AUTH_TOKEN_2) - .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()); + .thenReturn(TEST_INSTALLATION_TOKEN_RESULT); + + when(persistedFidReturnsError.insertOrUpdatePersistedFidEntry(any())).thenReturn(false); + when(persistedFidReturnsError.readPersistedFidEntryValue()) + .thenReturn(DEFAULT_PERSISTED_FID_ENTRY); + when(backendClientReturnsError.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenThrow( new FirebaseInstallationServiceException( "SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); - when(persistedFidReturnsError.insertOrUpdatePersistedFidEntry(any())).thenReturn(false); - when(persistedFidReturnsError.readPersistedFidEntryValue()).thenReturn(null); + when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); - when(mockClock.currentTimeMillis()).thenReturn(TEST_CREATION_TIMESTAMP_1); + when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2); + // Mocks success on FIS deletion doNothing() .when(backendClientReturnsOk) @@ -185,95 +182,77 @@ public void cleanUp() throws Exception { @Test public void testGetId_PersistedFidOk_BackendOk() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); // No exception, means success. - assertWithMessage("getId Task fails.") + assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) .isNotEmpty(); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(entryValue.getFirebaseInstallationId()) - .isEqualTo(TEST_FID_1); + assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(updatedFidEntry.getFirebaseInstallationId()) - .isEqualTo(TEST_FID_1); - assertWithMessage("Registration status doesn't match") - .that(updatedFidEntry.getRegistrationStatus()) - .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + assertThat(updatedFidEntry).hasFid(TEST_FID_1); + assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); - - // No exception, means success. - assertWithMessage("getId Task fails.") - .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(entryValue.getFirebaseInstallationId()) - .isEqualTo(TEST_FID_1); - - Tasks.await(firebaseInstallations.getId()); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + // Call getId multiple times + Task task1 = firebaseInstallations.getId(); + Task task2 = firebaseInstallations.getId(); + Tasks.await(Tasks.whenAllComplete(task1, task2)); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(updatedFidEntry.getFirebaseInstallationId()) + assertWithMessage("Persisted Fid of Task1 doesn't match.") + .that(task1.getResult()) .isEqualTo(TEST_FID_1); - assertWithMessage("Registration status doesn't match") - .that(updatedFidEntry.getRegistrationStatus()) - .isEqualTo(PersistedFid.RegistrationStatus.REGISTERED); + assertWithMessage("Persisted Fid of Task2 doesn't match.") + .that(task2.getResult()) + .isEqualTo(TEST_FID_1); + verify(backendClientReturnsOk, times(1)) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertThat(updatedFidEntry).hasFid(TEST_FID_1); + assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } @Test public void testGetId_PersistedFidOk_BackendError() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); Tasks.await(firebaseInstallations.getId()); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(entryValue.getFirebaseInstallationId()) - .isEqualTo(TEST_FID_1); + assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid doesn't match") - .that(updatedFidEntry.getFirebaseInstallationId()) - .isEqualTo(TEST_FID_1); - assertWithMessage("Registration Fid doesn't match") - .that(updatedFidEntry.getRegistrationStatus()) - .isEqualTo(PersistedFid.RegistrationStatus.REGISTER_ERROR); + assertThat(updatedFidEntry).hasFid(TEST_FID_1); + assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } @Test public void testGetId_PersistedFidError_BackendOk() throws InterruptedException { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, - executor, - firebaseApp, - backendClientReturnsOk, - persistedFidReturnsError, - mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFidReturnsError, mockUtils); // Expect exception try { @@ -290,30 +269,110 @@ public void testGetId_PersistedFidError_BackendOk() throws InterruptedException } } + @Test + public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { + // Mocking unchecked exception on FIS createFirebaseInstallation + when(mockClient.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenAnswer( + invocation -> { + throw new InterruptedException(); + }); + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations(executor, firebaseApp, mockClient, persistedFid, mockUtils); + + Tasks.await(firebaseInstallations.getId()); + + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // Validate that registration status is REGISTER_ERROR + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertThat(updatedFidEntry).hasFid(TEST_FID_1); + assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + } + + @Test + public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { + // Update local storage with fid entry that has auth token expired. + persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Mocking unchecked exception on FIS generateAuthToken + when(mockClient.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenAnswer( + invocation -> { + throw new InterruptedException(); + }); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations(executor, firebaseApp, mockClient, persistedFid, mockUtils); + + assertWithMessage("getId Task failed") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that generates auth token with the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // Validate that registration status is REGISTER_ERROR + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertThat(updatedFidEntry).hasFid(TEST_FID_1); + assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + } + + @Test + public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { + // Update local storage with fid entry that has auth token expired. + persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + + assertWithMessage("getId Task failed") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // Validate that Persisted FID has a refreshed auth token now + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertThat(updatedFidEntry).hasAuthToken(TEST_AUTH_TOKEN_2); + verify(backendClientReturnsOk, never()) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + verify(backendClientReturnsOk, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Auth Token doesn't match") - .that(entryValue.getAuthToken()) - .isEqualTo(TEST_AUTH_TOKEN); + assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); } @Test public void testGetAuthToken_PersistedFidError_failure() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, - executor, - firebaseApp, - backendClientReturnsOk, - persistedFidReturnsError, - mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFidReturnsError, mockUtils); // Expect exception try { @@ -327,16 +386,18 @@ public void testGetAuthToken_PersistedFidError_failure() throws Exception { .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); } } @Test public void testGetAuthToken_fidExists_successful() throws Exception { when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -345,14 +406,19 @@ public void testGetAuthToken_fidExists_successful() throws Exception { assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); + verify(backendClientReturnsOk, never()) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(EXPIRED_AUTH_TOKEN_ENTRY); + persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -361,18 +427,20 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); + verify(backendClientReturnsOk, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { - // Using mockPersistedFid to ensure the order of returning persistedFidEntry. This test - // validates that getAuthToken calls getId to ensure FID registration and returns a valid auth - // token. - when(mockPersistedFid.readPersistedFidEntryValue()) - .thenReturn(UNREGISTERED_FID_ENTRY, REGISTERED_FID_ENTRY); + // Update local storage with a unregistered fid entry to validate that getAuthToken calls getId + // to ensure FID registration and returns a valid auth token. + persistedFid.insertOrUpdatePersistedFidEntry(UNREGISTERED_FID_ENTRY); + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -381,6 +449,8 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); + verify(backendClientReturnsOk, times(1)) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); } @Test @@ -391,14 +461,11 @@ public void testGetAuthToken_serverError_failure() throws Exception { .thenThrow( new FirebaseInstallationServiceException( "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, - executor, - firebaseApp, - backendClientReturnsError, - mockPersistedFid, - mockUtils); + executor, firebaseApp, backendClientReturnsError, mockPersistedFid, mockUtils); // Expect exception try { @@ -418,18 +485,16 @@ public void testGetAuthToken_serverError_failure() throws Exception { @Test public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() throws Exception { - // Using mockPersistedFid to ensure the order of returning persistedFidEntry to 2 tasks - // triggered simultaneously. Task2 waits for Task1 to complete. On Task1 completion, task2 reads - // the UPDATED_AUTH_TOKEN_FID_ENTRY by Task1 on execution. - when(mockPersistedFid.readPersistedFidEntryValue()) - .thenReturn( - EXPIRED_AUTH_TOKEN_ENTRY, - EXPIRED_AUTH_TOKEN_ENTRY, - EXPIRED_AUTH_TOKEN_ENTRY, - UPDATED_AUTH_TOKEN_FID_ENTRY); + // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks + // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads + // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. + persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -451,33 +516,36 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( @Test public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); - // Use a mock ServiceClient for network calls with delay(1000ms) to ensure first task is not + persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() // and verify one task waits for another task to complete. doAnswer( AdditionalAnswers.answersWithDelay( - 1000, + 500, (unused) -> InstallationTokenResult.builder() .setToken(TEST_AUTH_TOKEN_3) - .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build())) .doAnswer( AdditionalAnswers.answersWithDelay( - 1000, + 500, (unused) -> InstallationTokenResult.builder() .setToken(TEST_AUTH_TOKEN_4) - .setTokenExpirationInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build())) .when(backendClientReturnsOk) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + when(mockUtils.isAuthTokenExpired(any())).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); // Call getAuthToken multiple times with FORCE_REFRESH option. Task task1 = @@ -492,9 +560,11 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th .isEqualTo(TEST_AUTH_TOKEN_3); assertWithMessage("Persisted Auth Token doesn't match") .that(task2.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_4); - verify(backendClientReturnsOk, times(2)) + .isEqualTo(TEST_AUTH_TOKEN_3); + verify(backendClientReturnsOk, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); + assertThat(updatedFidEntry).hasAuthToken(TEST_AUTH_TOKEN_3); } @Test @@ -503,12 +573,12 @@ public void testDelete_registeredFID_successful() throws Exception { persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); Tasks.await(firebaseInstallations.delete()); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); verify(backendClientReturnsOk, times(1)) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @@ -519,12 +589,12 @@ public void testDelete_unregisteredFID_successful() throws Exception { persistedFid.insertOrUpdatePersistedFidEntry(UNREGISTERED_FID_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); Tasks.await(firebaseInstallations.delete()); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); verify(backendClientReturnsOk, never()) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @@ -533,12 +603,12 @@ public void testDelete_unregisteredFID_successful() throws Exception { public void testDelete_emptyPersistedFidEntry_successful() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); Tasks.await(firebaseInstallations.delete()); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid Entry is not null.").that(entryValue).isNull(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); verify(backendClientReturnsOk, never()) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @@ -549,7 +619,7 @@ public void testDelete_serverError_failure() throws Exception { persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - mockClock, executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); // Expect exception try { @@ -564,9 +634,7 @@ public void testDelete_serverError_failure() throws Exception { .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertWithMessage("Persisted Fid Entry doesn't match") - .that(entryValue) - .isEqualTo(REGISTERED_FID_ENTRY); + assertThat(entryValue).isEqualTo(REGISTERED_FID_ENTRY); } } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index e77a2b1155b..1728cb56554 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -14,6 +14,9 @@ package com.google.firebase.installations; +import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.remote.InstallationResponse; + public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; @@ -35,5 +38,26 @@ public final class FisAndroidTestConstants { public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP_2 = 2000L; public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; - public static final long TEST_CREATION_TIMESTAMP_2 = 2000L; + public static final long TEST_CREATION_TIMESTAMP_2 = 2L; + + public static final PersistedFidEntry DEFAULT_PERSISTED_FID_ENTRY = + PersistedFidEntry.builder().build(); + public static final InstallationResponse TEST_INSTALLATION_RESPONSE = + InstallationResponse.builder() + .setName("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .build()) + .build(); + + public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN_2) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .build(); } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java index f6aa77f45de..f00b9447bdb 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java @@ -14,6 +14,7 @@ package com.google.firebase.installations.local; +import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_FID_ENTRY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; @@ -23,7 +24,6 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import androidx.test.core.app.ApplicationProvider; @@ -69,8 +69,8 @@ public void cleanUp() throws Exception { @Test public void testReadPersistedFidEntry_Null() { - assertNull(persistedFid0.readPersistedFidEntryValue()); - assertNull(persistedFid1.readPersistedFidEntryValue()); + assertThat(persistedFid0.readPersistedFidEntryValue()).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); + assertThat(persistedFid1.readPersistedFidEntryValue()).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); } @Test diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index b2390ec8f06..7eebd24933e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -14,13 +14,13 @@ package com.google.firebase.installations; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.common.util.Clock; import com.google.android.gms.common.util.DefaultClock; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.installations.local.PersistedFid; @@ -29,6 +29,9 @@ import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import com.google.firebase.installations.remote.InstallationResponse; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -51,31 +54,31 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final FirebaseInstallationServiceClient serviceClient; private final PersistedFid persistedFid; private final ExecutorService executor; - private final Clock clock; private final Utils utils; + private final Object lock = new Object(); - private static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = 3600L; // 1 hour - private static final long AWAIT_TIMEOUT_IN_SECS = 10L; + @GuardedBy("lock") + private boolean shouldRefreshAuthToken; + + @GuardedBy("lock") + private final List listeners = new ArrayList<>(); /** package private constructor. */ FirebaseInstallations(FirebaseApp firebaseApp) { this( - DefaultClock.getInstance(), new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), firebaseApp, new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), new PersistedFid(firebaseApp), - new Utils()); + new Utils(DefaultClock.getInstance())); } FirebaseInstallations( - Clock clock, ExecutorService executor, FirebaseApp firebaseApp, FirebaseInstallationServiceClient serviceClient, PersistedFid persistedFid, Utils utils) { - this.clock = clock; this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; @@ -106,6 +109,18 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { return (FirebaseInstallations) app.get(FirebaseInstallationsApi.class); } + /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getApplicationId() { + return firebaseApp.getOptions().getApplicationId(); + } + + /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ + @VisibleForTesting + String getName() { + return firebaseApp.getName(); + } + /** * Returns a globally unique identifier of this Firebase app installation. This is a url-safe * base64 string of a 128-bit integer. @@ -113,18 +128,9 @@ public static FirebaseInstallations getInstance(@NonNull FirebaseApp app) { @NonNull @Override public Task getId() { - return getId(null); - } - - /** - * Returns a globally unique identifier of this Firebase app installation.Also, updates the {@link - * AwaitListener} when the FID registration is complete. - */ - private Task getId(AwaitListener awaitListener) { - return Tasks.call(executor, this::getPersistedFid) - .continueWith(orElse(this::createAndPersistNewFid)) - .onSuccessTask( - persistedFidEntry -> registerFidIfNecessary(persistedFidEntry, awaitListener)); + Task task = addGetIdListener(); + executor.execute(this::doRegistration); + return task; } /** @@ -138,14 +144,10 @@ private Task getId(AwaitListener awaitListener) { */ @NonNull @Override - public synchronized Task getAuthToken( - @AuthTokenOption int authTokenOption) { - AwaitListener awaitListener = new AwaitListener(); - return getId(awaitListener) - .continueWith( - executor, - awaitFidRegistration( - () -> refreshAuthTokenIfNecessary(authTokenOption), awaitListener)); + public Task getAuthToken(@AuthTokenOption int authTokenOption) { + Task task = addGetAuthTokenListener(authTokenOption); + executor.execute(this::doRegistration); + return task; } /** @@ -159,64 +161,108 @@ public Task delete() { return Tasks.call(executor, this::deleteFirebaseInstallationId); } - /** Returns the application id of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ - @VisibleForTesting - String getApplicationId() { - return firebaseApp.getOptions().getApplicationId(); + private Task addGetIdListener() { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + StateListener l = new GetIdListener(taskCompletionSource); + synchronized (lock) { + listeners.add(l); + } + return taskCompletionSource.getTask(); } - /** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseInstallations} */ - @VisibleForTesting - String getName() { - return firebaseApp.getName(); + private Task addGetAuthTokenListener( + @AuthTokenOption int authTokenOption) { + TaskCompletionSource taskCompletionSource = + new TaskCompletionSource<>(); + StateListener l = new GetAuthTokenListener(utils, taskCompletionSource); + synchronized (lock) { + if (authTokenOption == FORCE_REFRESH) { + shouldRefreshAuthToken = true; + } + listeners.add(l); + } + return taskCompletionSource.getTask(); } - /** - * Returns the {@link PersistedFidEntry} from shared prefs. - * - * @throws {@link FirebaseInstallationsException} when shared pref is empty or {@link - * PersistedFidEntry} is in error state. - */ - private PersistedFidEntry getPersistedFid() throws FirebaseInstallationsException { - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); - if (persistedFidMissingOrInErrorState(persistedFidEntry)) { - throw new FirebaseInstallationsException( - "Failed to get existing fid.", FirebaseInstallationsException.Status.CLIENT_ERROR); + private void triggerOnStateReached(PersistedFidEntry persistedFidEntry) { + synchronized (lock) { + Iterator it = listeners.iterator(); + while (it.hasNext()) { + StateListener l = it.next(); + boolean doneListening = l.onStateReached(persistedFidEntry, shouldRefreshAuthToken); + if (doneListening) { + it.remove(); + } + } } - return persistedFidEntry; } - private static boolean persistedFidMissingOrInErrorState(PersistedFidEntry persistedFidEntry) { - return persistedFidEntry == null - || persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTER_ERROR; + private void triggerOnException(PersistedFidEntry persistedFidEntry, Exception exception) { + synchronized (lock) { + Iterator it = listeners.iterator(); + while (it.hasNext()) { + StateListener l = it.next(); + boolean doneListening = l.onException(persistedFidEntry, exception); + if (doneListening) { + it.remove(); + } + } + } } - @NonNull - private static Continuation orElse(@NonNull Supplier supplier) { - return t -> { - if (t.isSuccessful()) { - return (T) t.getResult(); + private final void doRegistration() { + try { + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + + // New FID needs to be created + if (persistedFidEntry.isErrored() || persistedFidEntry.isNotGenerated()) { + String fid = utils.createRandomFid(); + persistFid(fid); + persistedFidEntry = persistedFid.readPersistedFidEntryValue(); } - return supplier.get(); - }; - } - @NonNull - private static Continuation awaitFidRegistration( - @NonNull Supplier supplier, AwaitListener listener) { - return t -> { - // Waiting for Task that registers FID on the FIS Servers - listener.await(AWAIT_TIMEOUT_IN_SECS, TimeUnit.SECONDS); - return supplier.get(); - }; - } + triggerOnStateReached(persistedFidEntry); - /** Creates a random FID and persists it in the shared prefs with UNREGISTERED status. */ - private PersistedFidEntry createAndPersistNewFid() throws FirebaseInstallationsException { - String fid = utils.createRandomFid(); - persistFid(fid); - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); - return persistedFidEntry; + // FID needs to be registered + if (persistedFidEntry.isUnregistered()) { + registerAndSaveFid(persistedFidEntry); + persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + // Newly registered Fid will have valid auth token. No refresh required. + synchronized (lock) { + shouldRefreshAuthToken = false; + } + } + + // Don't notify the listeners at this point; we might as well make ure the auth token is up + // to date before letting them know. + + boolean needRefresh = utils.isAuthTokenExpired(persistedFidEntry); + if (!needRefresh) { + synchronized (lock) { + needRefresh = shouldRefreshAuthToken; + } + } + + // Refresh Auth token if needed + if (needRefresh) { + fetchAuthTokenFromServer(persistedFidEntry); + persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + synchronized (lock) { + shouldRefreshAuthToken = false; + } + } + + triggerOnStateReached(persistedFidEntry); + } catch (Exception e) { + PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + PersistedFidEntry errorFidEntry = + persistedFidEntry + .toBuilder() + .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) + .build(); + persistedFid.insertOrUpdatePersistedFidEntry(errorFidEntry); + triggerOnException(errorFidEntry, e); + } } private void persistFid(String fid) throws FirebaseInstallationsException { @@ -234,58 +280,11 @@ private void persistFid(String fid) throws FirebaseInstallationsException { } } - /** - * Registers the FID with FIS servers if FID is in UNREGISTERED state. - * - *

Updates FID registration status to PENDING to avoid multiple network calls to FIS Servers. - */ - private Task registerFidIfNecessary( - PersistedFidEntry persistedFidEntry, AwaitListener listener) { - String fid = persistedFidEntry.getFirebaseInstallationId(); - - // Check if the fid is unregistered - if (persistedFidEntry.getRegistrationStatus() == RegistrationStatus.UNREGISTERED) { - updatePersistedFidWithPendingStatus(fid); - executeFidRegistration(persistedFidEntry, listener); - } else { - updateAwaitListenerIfRegisteredFid(persistedFidEntry, listener); - } - - return Tasks.forResult(fid); - } - - private void updateAwaitListenerIfRegisteredFid( - PersistedFidEntry persistedFidEntry, AwaitListener listener) { - if (listener != null - && persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED) { - listener.onSuccess(); - } - } - - /** - * Registers the FID with FIS servers in a background thread and updates the listener on - * completion. - */ - private void executeFidRegistration(PersistedFidEntry persistedFidEntry, AwaitListener listener) { - Task task = Tasks.call(executor, () -> registerAndSaveFid(persistedFidEntry)); - if (listener != null) { - task.addOnCompleteListener(listener); - } - } - - private void updatePersistedFidWithPendingStatus(String fid) { - persistedFid.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.PENDING) - .build()); - } - /** Registers the created Fid with FIS servers and update the shared prefs. */ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) throws FirebaseInstallationsException { try { - long creationTime = currentTimeInSecs(); + long creationTime = utils.currentTimeInSecs(); InstallationResponse installationResponse = serviceClient.createFirebaseInstallation( @@ -299,70 +298,22 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) .setRegistrationStatus(RegistrationStatus.REGISTERED) .setAuthToken(installationResponse.getAuthToken().getToken()) .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationInSecs()) + .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) .setTokenCreationEpochInSecs(creationTime) .build()); } catch (FirebaseInstallationServiceException exception) { - persistedFid.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() - .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) - .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) - .build()); throw new FirebaseInstallationsException( exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); } return null; } - private InstallationTokenResult refreshAuthTokenIfNecessary(int authTokenOption) - throws FirebaseInstallationsException { - - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); - - if (!isPersistedFidRegistered(persistedFidEntry)) { - throw new FirebaseInstallationsException( - "Firebase Installation is not registered.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - } - - switch (authTokenOption) { - case FORCE_REFRESH: - return fetchAuthTokenFromServer(persistedFidEntry); - case DO_NOT_FORCE_REFRESH: - return getValidAuthToken(persistedFidEntry); - default: - throw new FirebaseInstallationsException( - "Incorrect refreshAuthTokenOption.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - } - } - - /** - * Returns a {@link InstallationTokenResult} created from the {@link PersistedFidEntry} if the - * auth token is valid else generates a new auth token by calling the FIS servers. - */ - private InstallationTokenResult getValidAuthToken(PersistedFidEntry persistedFidEntry) - throws FirebaseInstallationsException { - - return isAuthTokenExpired(persistedFidEntry) - ? fetchAuthTokenFromServer(persistedFidEntry) - : InstallationTokenResult.builder() - .setToken(persistedFidEntry.getAuthToken()) - .setTokenExpirationInSecs(persistedFidEntry.getExpiresInSecs()) - .build(); - } - - private boolean isPersistedFidRegistered(PersistedFidEntry persistedFidEntry) { - return persistedFidEntry != null - && persistedFidEntry.getRegistrationStatus() == RegistrationStatus.REGISTERED; - } - /** Calls the FIS servers to generate an auth token for this Firebase installation. */ private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persistedFidEntry) throws FirebaseInstallationsException { try { - long creationTime = currentTimeInSecs(); + long creationTime = utils.currentTimeInSecs(); InstallationTokenResult tokenResult = serviceClient.generateAuthToken( /*apiKey= */ firebaseApp.getOptions().getApiKey(), @@ -376,7 +327,7 @@ private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persi .setRegistrationStatus(RegistrationStatus.REGISTERED) .setAuthToken(tokenResult.getToken()) .setRefreshToken(persistedFidEntry.getRefreshToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationInSecs()) + .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) .setTokenCreationEpochInSecs(creationTime) .build()); @@ -388,19 +339,6 @@ private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persi } } - /** - * Checks if the FIS Auth token is expired or going to expire in next 1 hour - * (AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS). - */ - private boolean isAuthTokenExpired(PersistedFidEntry persistedFidEntry) { - return (persistedFidEntry.getTokenCreationEpochInSecs() + persistedFidEntry.getExpiresInSecs() - > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS); - } - - private long currentTimeInSecs() { - return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); - } - /** * Deletes the firebase installation id of the {@link FirebaseApp} from FIS servers and local * storage. @@ -409,8 +347,8 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); - if (isPersistedFidRegistered(persistedFidEntry)) { - // Call the FIS servers to delete this firebase installation id. + if (persistedFidEntry.isRegistered()) { + // Call the FIS servers to delete this Firebase Installation Id. try { serviceClient.deleteFirebaseInstallation( firebaseApp.getOptions().getApiKey(), @@ -429,7 +367,3 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio return null; } } - -interface Supplier { - T get() throws Exception; -} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java new file mode 100644 index 00000000000..008daadbe65 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.installations.local.PersistedFidEntry; + +class GetAuthTokenListener implements StateListener { + private final Utils utils; + private final TaskCompletionSource resultTaskCompletionSource; + + public GetAuthTokenListener( + Utils utils, TaskCompletionSource resultTaskCompletionSource) { + this.utils = utils; + this.resultTaskCompletionSource = resultTaskCompletionSource; + } + + @Override + public boolean onStateReached( + PersistedFidEntry persistedFidEntry, boolean shouldRefreshAuthToken) { + // AuthTokenListener state is reached when FID is registered and has a valid auth token + if (persistedFidEntry.isRegistered() + && !utils.isAuthTokenExpired(persistedFidEntry) + && !shouldRefreshAuthToken) { + resultTaskCompletionSource.setResult( + InstallationTokenResult.builder() + .setToken(persistedFidEntry.getAuthToken()) + .setTokenExpirationTimestamp(persistedFidEntry.getExpiresInSecs()) + .setTokenCreationTimestamp(persistedFidEntry.getTokenCreationEpochInSecs()) + .build()); + return true; + } + return false; + } + + @Override + public boolean onException(PersistedFidEntry persistedFidEntry, Exception exception) { + if (persistedFidEntry.isErrored()) { + resultTaskCompletionSource.trySetException(exception); + return true; + } + return false; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java new file mode 100644 index 00000000000..2e24b97cc07 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java @@ -0,0 +1,44 @@ +// 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.installations; + +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.installations.local.PersistedFidEntry; + +class GetIdListener implements StateListener { + final TaskCompletionSource taskCompletionSource; + + public GetIdListener(TaskCompletionSource taskCompletionSource) { + this.taskCompletionSource = taskCompletionSource; + } + + @Override + public boolean onStateReached(PersistedFidEntry persistedFidEntry, boolean unused) { + if (persistedFidEntry.isUnregistered() || persistedFidEntry.isRegistered()) { + taskCompletionSource.trySetResult(persistedFidEntry.getFirebaseInstallationId()); + return true; + } + return false; + } + + @Override + public boolean onException(PersistedFidEntry persistedFidEntry, Exception exception) { + if (persistedFidEntry.isErrored()) { + taskCompletionSource.trySetException(exception); + return true; + } + return false; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java new file mode 100644 index 00000000000..a9691f63d18 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java @@ -0,0 +1,31 @@ +// 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.installations; + +import com.google.firebase.installations.local.PersistedFidEntry; + +interface StateListener { + /** + * Returns {@code true} if the defined {@link PersistedFidEntry} state is reached, {@code false} + * otherwise. + */ + boolean onStateReached(PersistedFidEntry persistedFidEntry, boolean shouldRefreshAuthToken); + + /** + * Returns {@code true} if an exception is thrown while registering a Firebase Installation, + * {@code false} otherwise. + */ + boolean onException(PersistedFidEntry persistedFidEntry, Exception exception); +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index 1ad7efab2f2..1a3477e9894 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -15,13 +15,17 @@ package com.google.firebase.installations; import androidx.annotation.NonNull; +import com.google.android.gms.common.util.Clock; +import com.google.firebase.installations.local.PersistedFidEntry; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.UUID; +import java.util.concurrent.TimeUnit; /** Util methods used for {@link FirebaseInstallations} */ class Utils { + private final Clock clock; /** * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy @@ -38,6 +42,26 @@ class Utils { /** Length of new-format FIDs as introduced in 2019. */ public static final int FID_LENGTH = 22; + private static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1); + + Utils(Clock clock) { + this.clock = clock; + } + + /** + * Checks if the FIS Auth token is expired or going to expire in next 1 hour {@link + * #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}. + */ + public boolean isAuthTokenExpired(PersistedFidEntry persistedFidEntry) { + return persistedFidEntry.getTokenCreationEpochInSecs() + persistedFidEntry.getExpiresInSecs() + > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + } + + /** Returns current time in seconds. */ + public long currentTimeInSecs() { + return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); + } + /** * Creates a random FID of valid format without checking if the FID is already in use by any * Firebase Installation. diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java index 1da104fa350..808b277c467 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java @@ -16,8 +16,8 @@ import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.google.firebase.FirebaseApp; import java.util.Arrays; import java.util.List; @@ -31,14 +31,28 @@ public class PersistedFid { // NOTE: never change the ordinal of the enum values because the enum values are stored in shared // prefs as their ordinal numbers. public enum RegistrationStatus { - /** {@link PersistedFidEntry} is synced to FIS servers */ - REGISTERED, - /** {@link PersistedFidEntry} is not synced with FIS server */ + /** + * {@link PersistedFidEntry} default registration status. Next state: UNREGISTERED - A new FID + * is created and persisted locally before registering with FIS servers. + */ + NOT_GENERATED, + /** + * {@link PersistedFidEntry} is not synced with FIS servers. Next state: REGISTERED - If FID + * registration is successful. REGISTER_ERROR - If FID registration or refresh auth token + * failed. + */ UNREGISTERED, - /** {@link PersistedFidEntry} is in error state when syncing with FIS server */ + /** + * {@link PersistedFidEntry} is synced to FIS servers. Next state: REGISTER_ERROR - If FID + * registration or refresh auth token failed. + */ + REGISTERED, + /** + * {@link PersistedFidEntry} is in error state when an exception is thrown while syncing with + * FIS server. Next state: UNREGISTERED - A new FID is created and persisted locally before + * registering with FIS servers. + */ REGISTER_ERROR, - /** {@link PersistedFidEntry} is in pending state when waiting for FIS server response */ - PENDING } private static final String SHARED_PREFS_NAME = "PersistedFid"; @@ -59,7 +73,9 @@ public enum RegistrationStatus { EXPIRES_IN_SECONDS_KEY, PERSISTED_STATUS_KEY); + @GuardedBy("prefs") private final SharedPreferences prefs; + private final String persistenceKey; public PersistedFid(@NonNull FirebaseApp firebaseApp) { @@ -72,30 +88,29 @@ public PersistedFid(@NonNull FirebaseApp firebaseApp) { persistenceKey = firebaseApp.getPersistenceKey(); } - @Nullable + @NonNull public PersistedFidEntry readPersistedFidEntryValue() { - String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); - String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); - String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); - long tokenCreationTime = - prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); - long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + synchronized (prefs) { + String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); + int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); + String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); + String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); + long tokenCreationTime = + prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); + long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - if (fid == null - || status == -1 - || !(status >= 0 && status < RegistrationStatus.values().length)) { - return null; + if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { + return PersistedFidEntry.builder().build(); + } + return PersistedFidEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .build(); } - - return PersistedFidEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.values()[status]) - .setAuthToken(authToken) - .setRefreshToken(refreshToken) - .setTokenCreationEpochInSecs(tokenCreationTime) - .setExpiresInSecs(expiresIn) - .build(); } @NonNull diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java index 4c8a9a56ec4..4019967cdb6 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java @@ -25,7 +25,7 @@ @AutoValue public abstract class PersistedFidEntry { - @NonNull + @Nullable public abstract String getFirebaseInstallationId(); @NonNull @@ -41,6 +41,22 @@ public abstract class PersistedFidEntry { public abstract long getTokenCreationEpochInSecs(); + public boolean isRegistered() { + return getRegistrationStatus() == PersistedFid.RegistrationStatus.REGISTERED; + } + + public boolean isErrored() { + return getRegistrationStatus() == PersistedFid.RegistrationStatus.REGISTER_ERROR; + } + + public boolean isUnregistered() { + return getRegistrationStatus() == PersistedFid.RegistrationStatus.UNREGISTERED; + } + + public boolean isNotGenerated() { + return getRegistrationStatus() == PersistedFid.RegistrationStatus.NOT_GENERATED; + } + @NonNull public abstract Builder toBuilder(); @@ -49,6 +65,7 @@ public abstract class PersistedFidEntry { public static PersistedFidEntry.Builder builder() { return new AutoValue_PersistedFidEntry.Builder() .setTokenCreationEpochInSecs(0) + .setRegistrationStatus(PersistedFid.RegistrationStatus.NOT_GENERATED) .setExpiresInSecs(0); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 51cc1869fae..ddb43292471 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -15,6 +15,7 @@ package com.google.firebase.installations.remote; import static android.content.ContentValues.TAG; +import static com.google.android.gms.common.internal.Preconditions.checkArgument; import android.content.Context; import android.content.pm.PackageManager; @@ -23,12 +24,13 @@ import androidx.annotation.NonNull; import com.google.android.gms.common.util.AndroidUtilsLight; import com.google.android.gms.common.util.Hex; +import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.installations.InstallationTokenResult; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.Charset; -import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HttpsURLConnection; import org.json.JSONException; @@ -61,6 +63,11 @@ public class FirebaseInstallationServiceClient { private static final int NETWORK_TIMEOUT_MILLIS = 10000; + private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s"); + + @VisibleForTesting + static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp."; + private final Context context; public FirebaseInstallationServiceClient(@NonNull Context context) { @@ -272,8 +279,8 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws if (key.equals("token")) { installationTokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { - installationTokenResult.setTokenExpirationInSecs( - TimeUnit.MILLISECONDS.toSeconds(reader.nextLong())); + installationTokenResult.setTokenExpirationTimestamp( + parseTokenExpirationTimestamp(reader.nextString())); } else { reader.skipValue(); } @@ -301,7 +308,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection if (name.equals("token")) { builder.setToken(reader.nextString()); } else if (name.equals("expiresIn")) { - builder.setTokenExpirationInSecs(TimeUnit.MILLISECONDS.toSeconds(reader.nextLong())); + builder.setTokenExpirationTimestamp(parseTokenExpirationTimestamp(reader.nextString())); } else { reader.skipValue(); } @@ -329,4 +336,19 @@ private String getFingerprintHashForPackage() { return null; } } + + /** + * Returns parsed token expiration timestamp in seconds. + * + * @param expiresIn is expiration timestamp in String format: 604800s + */ + @VisibleForTesting + static long parseTokenExpirationTimestamp(String expiresIn) { + checkArgument( + EXPIRATION_TIMESTAMP_PATTERN.matcher(expiresIn).matches(), + PARSING_EXPIRATION_TIME_ERROR_MESSAGE); + return (expiresIn == null || expiresIn.length() == 0) + ? 0L + : Long.parseLong(expiresIn.substring(0, expiresIn.length() - 1)); + } } diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java new file mode 100644 index 00000000000..eb30312c0e4 --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClientTest.java @@ -0,0 +1,55 @@ +// 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.installations.remote; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseInstallationServiceClient}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseInstallationServiceClientTest { + + private final String TEST_EXPIRATION_TIMESTAMP = "604800s"; + private final long TEST_EXPIRATION_IN_SECS = 604800; + private final String INCORRECT_EXPIRATION_TIMESTAMP = "2345"; + + @Test + public void parseTokenExpirationTimestamp_successful() { + long actual = + FirebaseInstallationServiceClient.parseTokenExpirationTimestamp(TEST_EXPIRATION_TIMESTAMP); + + assertWithMessage("Exception status doesn't match") + .that(actual) + .isEqualTo(TEST_EXPIRATION_IN_SECS); + } + + @Test + public void parseTokenExpirationTimestamp_failed() { + try { + FirebaseInstallationServiceClient.parseTokenExpirationTimestamp( + INCORRECT_EXPIRATION_TIMESTAMP); + fail("Parsing token expiration timestamp failed."); + } catch (IllegalArgumentException expected) { + assertThat(expected) + .hasMessageThat() + .isEqualTo(FirebaseInstallationServiceClient.PARSING_EXPIRATION_TIME_ERROR_MESSAGE); + } + } +} From 8e65eaed5cf21caccf7f96a8f5f1fa0f019b2442 Mon Sep 17 00:00:00 2001 From: Ankita Date: Mon, 14 Oct 2019 14:31:15 -0700 Subject: [PATCH 59/74] Merging changes into fis_sdk (#910) * Fixing FISClient to correctly parse expiration timestamp. (#848) * Updating getAuthToken to return creation timestamp (#884) * Propagating the exceptions to the clients. (#856) * Extract FID from FIS createInstallation response (#888) * Addressing Rayo's comment: Rename PersistedFidEntry to (#899) * Implementing retry once for FIS Client. (#895) * Simplifying FirebaseInstallations class by adding listeners. (#847) * Fixing FISClient to correctly parse expiration timestamp. (#848) * Updating getAuthToken to return creation timestamp (#884) * Propagating the exceptions to the clients. (#856) --- firebase-installations/api.txt | 71 ++--- ...FirebaseInstallationsInstrumentedTest.java | 301 +++++++++++------- .../FisAndroidTestConstants.java | 11 +- ...=> PersistedInstallationEntrySubject.java} | 31 +- ...st.java => PersistedInstallationTest.java} | 51 +-- .../installations/FirebaseInstallations.java | 107 ++++--- .../installations/GetAuthTokenListener.java | 19 +- .../firebase/installations/GetIdListener.java | 14 +- .../firebase/installations/StateListener.java | 11 +- .../google/firebase/installations/Utils.java | 7 +- ...tedFid.java => PersistedInstallation.java} | 33 +- ...y.java => PersistedInstallationEntry.java} | 29 +- .../FirebaseInstallationServiceClient.java | 173 +++++----- .../FirebaseInstallationServiceException.java | 56 ---- .../remote/InstallationResponse.java | 10 +- 15 files changed, 475 insertions(+), 449 deletions(-) rename firebase-installations/src/androidTest/java/com/google/firebase/installations/local/{PersistedFidEntrySubject.java => PersistedInstallationEntrySubject.java} (65%) rename firebase-installations/src/androidTest/java/com/google/firebase/installations/local/{PersistedFidTest.java => PersistedInstallationTest.java} (69%) rename firebase-installations/src/main/java/com/google/firebase/installations/local/{PersistedFid.java => PersistedInstallation.java} (79%) rename firebase-installations/src/main/java/com/google/firebase/installations/local/{PersistedFidEntry.java => PersistedInstallationEntry.java} (63%) delete mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 1514498e651..4e676ce4849 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -25,45 +25,45 @@ package com.google.firebase.installations { package com.google.firebase.installations.local { - public class PersistedFid { - ctor public PersistedFid(@NonNull FirebaseApp); + public class PersistedInstallation { + ctor public PersistedInstallation(@NonNull FirebaseApp); method @NonNull public boolean clear(); - method @NonNull public boolean insertOrUpdatePersistedFidEntry(@NonNull com.google.firebase.installations.local.PersistedFidEntry); - method @NonNull public com.google.firebase.installations.local.PersistedFidEntry readPersistedFidEntryValue(); + method @NonNull public boolean insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry readPersistedInstallationEntryValue(); } - public enum PersistedFid.RegistrationStatus { - enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus NOT_GENERATED; - enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTERED; - enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus REGISTER_ERROR; - enum_constant public static final com.google.firebase.installations.local.PersistedFid.RegistrationStatus UNREGISTERED; + public enum PersistedInstallation.RegistrationStatus { + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus NOT_GENERATED; + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTERED; + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTER_ERROR; + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus UNREGISTERED; } - public abstract class PersistedFidEntry { - ctor public PersistedFidEntry(); - method @NonNull public static com.google.firebase.installations.local.PersistedFidEntry.Builder builder(); + public abstract class PersistedInstallationEntry { + ctor public PersistedInstallationEntry(); + method @NonNull public static com.google.firebase.installations.local.PersistedInstallationEntry.Builder builder(); method @Nullable public abstract String getAuthToken(); method public abstract long getExpiresInSecs(); method @Nullable public abstract String getFirebaseInstallationId(); method @Nullable public abstract String getRefreshToken(); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFid.RegistrationStatus getRegistrationStatus(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus getRegistrationStatus(); method public abstract long getTokenCreationEpochInSecs(); method public boolean isErrored(); method public boolean isNotGenerated(); method public boolean isRegistered(); method public boolean isUnregistered(); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder toBuilder(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder toBuilder(); } - public abstract static class PersistedFidEntry.Builder { - ctor public PersistedFidEntry.Builder(); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry build(); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setAuthToken(@Nullable String); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setExpiresInSecs(long); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setFirebaseInstallationId(@NonNull String); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setRefreshToken(@Nullable String); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setRegistrationStatus(@NonNull com.google.firebase.installations.local.PersistedFid.RegistrationStatus); - method @NonNull public abstract com.google.firebase.installations.local.PersistedFidEntry.Builder setTokenCreationEpochInSecs(long); + public abstract static class PersistedInstallationEntry.Builder { + ctor public PersistedInstallationEntry.Builder(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry build(); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setAuthToken(@Nullable String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setExpiresInSecs(long); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setFirebaseInstallationId(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setRefreshToken(@Nullable String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setRegistrationStatus(@NonNull com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setTokenCreationEpochInSecs(long); } } @@ -72,30 +72,18 @@ package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { ctor public FirebaseInstallationServiceClient(@NonNull Context); - method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - } - - public class FirebaseInstallationServiceException { - ctor public FirebaseInstallationServiceException(@NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); - ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); - ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status, @NonNull Throwable); - method @NonNull public com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status getStatus(); - } - - public enum FirebaseInstallationServiceException.Status { - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status NETWORK_ERROR; - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status SERVER_ERROR; - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status UNAUTHORIZED; + method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); } public abstract class InstallationResponse { ctor public InstallationResponse(); method @NonNull public static com.google.firebase.installations.remote.InstallationResponse.Builder builder(); method @NonNull public abstract InstallationTokenResult getAuthToken(); - method @NonNull public abstract String getName(); + method @NonNull public abstract String getFid(); method @NonNull public abstract String getRefreshToken(); + method @NonNull public abstract String getUri(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder toBuilder(); } @@ -103,8 +91,9 @@ package com.google.firebase.installations.remote { ctor public InstallationResponse.Builder(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse build(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull InstallationTokenResult); - method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setName(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setFid(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setRefreshToken(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setUri(@NonNull String); } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index bd6ef7eb292..d64d34ffc72 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -15,7 +15,8 @@ package com.google.firebase.installations; import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_FID_ENTRY; +import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_INSTALLATION_ENTRY; +import static com.google.firebase.installations.FisAndroidTestConstants.INVALID_TEST_FID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; @@ -31,7 +32,7 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; -import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; +import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -48,12 +49,12 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.local.PersistedFid; -import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallation; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -79,56 +80,66 @@ public class FirebaseInstallationsInstrumentedTest { private FirebaseApp firebaseApp; private ExecutorService executor; - private PersistedFid persistedFid; + private PersistedInstallation persistedInstallation; @Mock private FirebaseInstallationServiceClient backendClientReturnsOk; @Mock private FirebaseInstallationServiceClient backendClientReturnsError; - @Mock private PersistedFid persistedFidReturnsError; + @Mock private PersistedInstallation persistedInstallationReturnsError; @Mock private Utils mockUtils; - @Mock private PersistedFid mockPersistedFid; + @Mock private PersistedInstallation mockPersistedInstallation; @Mock private FirebaseInstallationServiceClient mockClient; - private static final PersistedFidEntry REGISTERED_FID_ENTRY = - PersistedFidEntry.builder() + private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) .setRefreshToken(TEST_REFRESH_TOKEN) .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); - private static final PersistedFidEntry EXPIRED_AUTH_TOKEN_ENTRY = - PersistedFidEntry.builder() + private static final PersistedInstallationEntry EXPIRED_AUTH_TOKEN_ENTRY = + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) .setRefreshToken(TEST_REFRESH_TOKEN) .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) - .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); - private static final PersistedFidEntry UNREGISTERED_FID_ENTRY = - PersistedFidEntry.builder() + private static final PersistedInstallationEntry UNREGISTERED_INSTALLATION_ENTRY = + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken("") .setRefreshToken("") .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) .setExpiresInSecs(0) - .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) .build(); - private static final PersistedFidEntry UPDATED_AUTH_TOKEN_ENTRY = - PersistedFidEntry.builder() + private static final PersistedInstallationEntry INVALID_INSTALLATION_ENTRY = + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(INVALID_TEST_FID) + .setAuthToken("") + .setRefreshToken("") + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) + .setExpiresInSecs(0) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) + .build(); + + private static final PersistedInstallationEntry UPDATED_AUTH_TOKEN_ENTRY = + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN_2) .setRefreshToken(TEST_REFRESH_TOKEN) .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); @Before - public void setUp() throws FirebaseInstallationServiceException { + public void setUp() throws FirebaseException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); @@ -140,7 +151,7 @@ public void setUp() throws FirebaseInstallationServiceException { .setProjectId(TEST_PROJECT_ID) .setApiKey(TEST_API_KEY) .build()); - persistedFid = new PersistedFid(firebaseApp); + persistedInstallation = new PersistedInstallation(firebaseApp); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) @@ -150,15 +161,14 @@ public void setUp() throws FirebaseInstallationServiceException { anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_TOKEN_RESULT); - when(persistedFidReturnsError.insertOrUpdatePersistedFidEntry(any())).thenReturn(false); - when(persistedFidReturnsError.readPersistedFidEntryValue()) - .thenReturn(DEFAULT_PERSISTED_FID_ENTRY); + when(persistedInstallationReturnsError.insertOrUpdatePersistedInstallationEntry(any())) + .thenReturn(false); + when(persistedInstallationReturnsError.readPersistedInstallationEntryValue()) + .thenReturn(DEFAULT_PERSISTED_INSTALLATION_ENTRY); when(backendClientReturnsError.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) - .thenThrow( - new FirebaseInstallationServiceException( - "SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + .thenThrow(new FirebaseException("SDK Error")); when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2); @@ -168,46 +178,49 @@ public void setUp() throws FirebaseInstallationServiceException { .when(backendClientReturnsOk) .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); // Mocks server error on FIS deletion - doThrow( - new FirebaseInstallationServiceException( - "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)) + doThrow(new FirebaseException("Server Error")) .when(backendClientReturnsError) .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @After public void cleanUp() throws Exception { - persistedFid.clear(); + persistedInstallation.clear(); } @Test - public void testGetId_PersistedFidOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); // No exception, means success. assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) .isNotEmpty(); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasFid(TEST_FID_1); - assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); // Call getId multiple times Task task1 = firebaseInstallations.getId(); @@ -224,35 +237,69 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { .isEqualTo(TEST_FID_1); verify(backendClientReturnsOk, times(1)) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasFid(TEST_FID_1); - assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } @Test - public void testGetId_PersistedFidOk_BackendError() throws Exception { + public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { + // Update local storage with installation entry that has invalid fid. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + + // No exception, means success. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(INVALID_TEST_FID); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + // After FID registration is complete, installation entry is updated with valid fid. + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + } + + @Test + public void testGetId_PersistedInstallationOk_BackendError() throws Exception { + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.getId()); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasFid(TEST_FID_1); - assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } @Test - public void testGetId_PersistedFidError_BackendOk() throws InterruptedException { + public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedException { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFidReturnsError, mockUtils); + executor, + firebaseApp, + backendClientReturnsOk, + persistedInstallationReturnsError, + mockUtils); // Expect exception try { @@ -277,29 +324,32 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E invocation -> { throw new InterruptedException(); }); - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = - new FirebaseInstallations(executor, firebaseApp, mockClient, persistedFid, mockUtils); + new FirebaseInstallations( + executor, firebaseApp, mockClient, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.getId()); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); // Validate that registration status is REGISTER_ERROR - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasFid(TEST_FID_1); - assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } @Test public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { - // Update local storage with fid entry that has auth token expired. - persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Update local storage with installation entry that has auth token expired. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); // Mocking unchecked exception on FIS generateAuthToken when(mockClient.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenAnswer( @@ -309,45 +359,50 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); FirebaseInstallations firebaseInstallations = - new FirebaseInstallations(executor, firebaseApp, mockClient, persistedFid, mockUtils); + new FirebaseInstallations( + executor, firebaseApp, mockClient, persistedInstallation, mockUtils); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) .isNotEmpty(); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that generates auth token with the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); // Validate that registration status is REGISTER_ERROR - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasFid(TEST_FID_1); - assertThat(updatedFidEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } @Test public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { - // Update local storage with fid entry that has auth token expired. - persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Update local storage with installation entry that has auth token expired. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) .isNotEmpty(); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); // Validate that Persisted FID has a refreshed auth token now - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasAuthToken(TEST_AUTH_TOKEN_2); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_2); verify(backendClientReturnsOk, never()) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); verify(backendClientReturnsOk, times(1)) @@ -356,23 +411,28 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); } @Test - public void testGetAuthToken_PersistedFidError_failure() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + public void testGetAuthToken_PersistedInstallationError_failure() throws Exception { + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFidReturnsError, mockUtils); + executor, + firebaseApp, + backendClientReturnsOk, + persistedInstallationReturnsError, + mockUtils); // Expect exception try { @@ -392,12 +452,13 @@ public void testGetAuthToken_PersistedFidError_failure() throws Exception { @Test public void testGetAuthToken_fidExists_successful() throws Exception { - when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + when(mockPersistedInstallation.readPersistedInstallationEntryValue()) + .thenReturn(REGISTERED_INSTALLATION_ENTRY); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, mockPersistedInstallation, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -412,13 +473,13 @@ public void testGetAuthToken_fidExists_successful() throws Exception { @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -433,14 +494,14 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex @Test public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { - // Update local storage with a unregistered fid entry to validate that getAuthToken calls getId - // to ensure FID registration and returns a valid auth token. - persistedFid.insertOrUpdatePersistedFidEntry(UNREGISTERED_FID_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + // Update local storage with a unregistered installation entry to validate that getAuthToken + // calls getId to ensure FID registration and returns a valid auth token. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); InstallationTokenResult installationTokenResult = Tasks.await( @@ -455,17 +516,16 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc @Test public void testGetAuthToken_serverError_failure() throws Exception { - when(mockPersistedFid.readPersistedFidEntryValue()).thenReturn(REGISTERED_FID_ENTRY); + when(mockPersistedInstallation.readPersistedInstallationEntryValue()) + .thenReturn(REGISTERED_INSTALLATION_ENTRY); when(backendClientReturnsError.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenThrow( - new FirebaseInstallationServiceException( - "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); - when(mockUtils.isAuthTokenExpired(REGISTERED_FID_ENTRY)).thenReturn(false); + .thenThrow(new FirebaseException("Server Error")); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, mockPersistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsError, mockPersistedInstallation, mockUtils); // Expect exception try { @@ -488,13 +548,13 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. - persistedFid.insertOrUpdatePersistedFidEntry(EXPIRED_AUTH_TOKEN_ENTRY); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -516,7 +576,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( @Test public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() // and verify one task waits for another task to complete. @@ -545,7 +605,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); // Call getAuthToken multiple times with FORCE_REFRESH option. Task task1 = @@ -563,38 +623,41 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th .isEqualTo(TEST_AUTH_TOKEN_3); verify(backendClientReturnsOk, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - PersistedFidEntry updatedFidEntry = persistedFid.readPersistedFidEntryValue(); - assertThat(updatedFidEntry).hasAuthToken(TEST_AUTH_TOKEN_3); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_3); } @Test public void testDelete_registeredFID_successful() throws Exception { - // Update local storage with a registered fid entry - persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.delete()); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); verify(backendClientReturnsOk, times(1)) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testDelete_unregisteredFID_successful() throws Exception { - // Update local storage with a unregistered fid entry - persistedFid.insertOrUpdatePersistedFidEntry(UNREGISTERED_FID_ENTRY); + // Update local storage with a unregistered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.delete()); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); verify(backendClientReturnsOk, never()) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @@ -603,23 +666,24 @@ public void testDelete_unregisteredFID_successful() throws Exception { public void testDelete_emptyPersistedFidEntry_successful() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); Tasks.await(firebaseInstallations.delete()); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); verify(backendClientReturnsOk, never()) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testDelete_serverError_failure() throws Exception { - // Update local storage with a registered fid entry - persistedFid.insertOrUpdatePersistedFidEntry(REGISTERED_FID_ENTRY); + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedFid, mockUtils); + executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); // Expect exception try { @@ -633,8 +697,9 @@ public void testDelete_serverError_failure() throws Exception { assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - PersistedFidEntry entryValue = persistedFid.readPersistedFidEntryValue(); - assertThat(entryValue).isEqualTo(REGISTERED_FID_ENTRY); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); } } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 1728cb56554..d94cdcc214c 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -14,11 +14,13 @@ package com.google.firebase.installations; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.InstallationResponse; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + /** Invalid FID. */ + public static final String INVALID_TEST_FID = "invalid"; public static final String TEST_PROJECT_ID = "777777777777"; @@ -40,11 +42,12 @@ public final class FisAndroidTestConstants { public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; - public static final PersistedFidEntry DEFAULT_PERSISTED_FID_ENTRY = - PersistedFidEntry.builder().build(); + public static final PersistedInstallationEntry DEFAULT_PERSISTED_INSTALLATION_ENTRY = + PersistedInstallationEntry.builder().build(); public static final InstallationResponse TEST_INSTALLATION_RESPONSE = InstallationResponse.builder() - .setName("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setFid(TEST_FID_1) .setRefreshToken(TEST_REFRESH_TOKEN) .setAuthToken( InstallationTokenResult.builder() diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java similarity index 65% rename from firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java rename to firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java index 48586508823..84c738ef42c 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidEntrySubject.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java @@ -18,37 +18,40 @@ import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; -import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import org.checkerframework.checker.nullness.compatqual.NullableDecl; -public final class PersistedFidEntrySubject extends Subject { +public final class PersistedInstallationEntrySubject extends Subject { // User-defined entry point - public static PersistedFidEntrySubject assertThat( - @NullableDecl PersistedFidEntry persistedFidEntry) { - return assertAbout(PERSISTED_FID_ENTRY_SUBJECT_FACTORY).that(persistedFidEntry); + public static PersistedInstallationEntrySubject assertThat( + @NullableDecl PersistedInstallationEntry persistedInstallationEntry) { + return assertAbout(PERSISTED_INSTALLATION_ENTRY_SUBJECT_FACTORY) + .that(persistedInstallationEntry); } // Static method for getting the subject factory (for use with assertAbout()) - public static Subject.Factory persistedFidEntry() { - return PERSISTED_FID_ENTRY_SUBJECT_FACTORY; + public static Subject.Factory + persistedInstallationEntry() { + return PERSISTED_INSTALLATION_ENTRY_SUBJECT_FACTORY; } - // Boiler-plate Subject.Factory for EmployeeSubject - private static final Subject.Factory - PERSISTED_FID_ENTRY_SUBJECT_FACTORY = PersistedFidEntrySubject::new; + // Boiler-plate Subject.Factory for PersistedInstallationEntrySubject + private static final Subject.Factory< + PersistedInstallationEntrySubject, PersistedInstallationEntry> + PERSISTED_INSTALLATION_ENTRY_SUBJECT_FACTORY = PersistedInstallationEntrySubject::new; - private final PersistedFidEntry actual; + private final PersistedInstallationEntry actual; /** * Constructor for use by subclasses. If you want to create an instance of this class itself, call - * {@link Subject#check(String, PersistedFidEntry ..) check(...)}{@code .that(actual)}. + * {@link Subject#check(String, PersistedInstallationEntry ..) check(...)}{@code .that(actual)}. * * @param metadata * @param actual */ - protected PersistedFidEntrySubject( - FailureMetadata metadata, @NullableDecl PersistedFidEntry actual) { + protected PersistedInstallationEntrySubject( + FailureMetadata metadata, @NullableDecl PersistedInstallationEntry actual) { super(metadata, actual); this.actual = actual; } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java similarity index 69% rename from firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java rename to firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java index f00b9447bdb..a27e1885924 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedFidTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java @@ -14,7 +14,7 @@ package com.google.firebase.installations.local; -import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_FID_ENTRY; +import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_INSTALLATION_ENTRY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; @@ -23,27 +23,27 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; -import static com.google.firebase.installations.local.PersistedFidEntrySubject.assertThat; +import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; import static org.junit.Assert.assertTrue; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Instrumented tests for {@link PersistedFid} */ +/** Instrumented tests for {@link PersistedInstallation} */ @RunWith(AndroidJUnit4.class) -public class PersistedFidTest { +public class PersistedInstallationTest { private FirebaseApp firebaseApp0; private FirebaseApp firebaseApp1; - private PersistedFid persistedFid0; - private PersistedFid persistedFid1; + private PersistedInstallation persistedInstallation0; + private PersistedInstallation persistedInstallation1; @Before public void setUp() { @@ -57,36 +57,39 @@ public void setUp() { ApplicationProvider.getApplicationContext(), new FirebaseOptions.Builder().setApplicationId(TEST_APP_ID_2).build(), "firebase_app_1"); - persistedFid0 = new PersistedFid(firebaseApp0); - persistedFid1 = new PersistedFid(firebaseApp1); + persistedInstallation0 = new PersistedInstallation(firebaseApp0); + persistedInstallation1 = new PersistedInstallation(firebaseApp1); } @After public void cleanUp() throws Exception { - persistedFid0.clear(); - persistedFid1.clear(); + persistedInstallation0.clear(); + persistedInstallation1.clear(); } @Test - public void testReadPersistedFidEntry_Null() { - assertThat(persistedFid0.readPersistedFidEntryValue()).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); - assertThat(persistedFid1.readPersistedFidEntryValue()).isEqualTo(DEFAULT_PERSISTED_FID_ENTRY); + public void testReadPersistedInstallationEntry_Null() { + assertThat(persistedInstallation0.readPersistedInstallationEntryValue()) + .isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); + assertThat(persistedInstallation1.readPersistedInstallationEntryValue()) + .isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); } @Test - public void testUpdateAndReadPersistedFidEntry_successful() throws Exception { - // Insert Persisted Fid Entry with Unregistered status in Shared Prefs + public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exception { + // Insert Persisted Installation Entry with Unregistered status in Shared Prefs assertTrue( - persistedFid0.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedFid.RegistrationStatus.UNREGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) .build())); - PersistedFidEntry entryValue = persistedFid0.readPersistedFidEntryValue(); + PersistedInstallationEntry entryValue = + persistedInstallation0.readPersistedInstallationEntryValue(); // Validate insertion was successful assertThat(entryValue).hasFid(TEST_FID_1); @@ -98,16 +101,16 @@ public void testUpdateAndReadPersistedFidEntry_successful() throws Exception { // Update Persisted Fid Entry with Registered status in Shared Prefs assertTrue( - persistedFid0.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) .setAuthToken(TEST_AUTH_TOKEN) .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedFid.RegistrationStatus.REGISTERED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) .build())); - entryValue = persistedFid0.readPersistedFidEntryValue(); + entryValue = persistedInstallation0.readPersistedInstallationEntryValue(); // Validate update was successful assertThat(entryValue).hasFid(TEST_FID_1); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 7eebd24933e..709b78637a8 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -23,11 +23,11 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; -import com.google.firebase.installations.local.PersistedFid; -import com.google.firebase.installations.local.PersistedFid.RegistrationStatus; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.FirebaseException; +import com.google.firebase.installations.local.PersistedInstallation; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import com.google.firebase.installations.remote.InstallationResponse; import java.util.ArrayList; import java.util.Iterator; @@ -52,7 +52,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final FirebaseApp firebaseApp; private final FirebaseInstallationServiceClient serviceClient; - private final PersistedFid persistedFid; + private final PersistedInstallation persistedInstallation; private final ExecutorService executor; private final Utils utils; private final Object lock = new Object(); @@ -69,7 +69,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), firebaseApp, new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), - new PersistedFid(firebaseApp), + new PersistedInstallation(firebaseApp), new Utils(DefaultClock.getInstance())); } @@ -77,12 +77,12 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { ExecutorService executor, FirebaseApp firebaseApp, FirebaseInstallationServiceClient serviceClient, - PersistedFid persistedFid, + PersistedInstallation persistedInstallation, Utils utils) { this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; - this.persistedFid = persistedFid; + this.persistedInstallation = persistedInstallation; this.utils = utils; } @@ -184,12 +184,13 @@ private Task addGetAuthTokenListener( return taskCompletionSource.getTask(); } - private void triggerOnStateReached(PersistedFidEntry persistedFidEntry) { + private void triggerOnStateReached(PersistedInstallationEntry persistedInstallationEntry) { synchronized (lock) { Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = l.onStateReached(persistedFidEntry, shouldRefreshAuthToken); + boolean doneListening = + l.onStateReached(persistedInstallationEntry, shouldRefreshAuthToken); if (doneListening) { it.remove(); } @@ -197,12 +198,13 @@ private void triggerOnStateReached(PersistedFidEntry persistedFidEntry) { } } - private void triggerOnException(PersistedFidEntry persistedFidEntry, Exception exception) { + private void triggerOnException( + PersistedInstallationEntry persistedInstallationEntry, Exception exception) { synchronized (lock) { Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = l.onException(persistedFidEntry, exception); + boolean doneListening = l.onException(persistedInstallationEntry, exception); if (doneListening) { it.remove(); } @@ -212,21 +214,22 @@ private void triggerOnException(PersistedFidEntry persistedFidEntry, Exception e private final void doRegistration() { try { - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry persistedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); // New FID needs to be created - if (persistedFidEntry.isErrored() || persistedFidEntry.isNotGenerated()) { + if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { String fid = utils.createRandomFid(); persistFid(fid); - persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); } - triggerOnStateReached(persistedFidEntry); + triggerOnStateReached(persistedInstallationEntry); // FID needs to be registered - if (persistedFidEntry.isUnregistered()) { - registerAndSaveFid(persistedFidEntry); - persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + if (persistedInstallationEntry.isUnregistered()) { + registerAndSaveFid(persistedInstallationEntry); + persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); // Newly registered Fid will have valid auth token. No refresh required. synchronized (lock) { shouldRefreshAuthToken = false; @@ -236,7 +239,7 @@ private final void doRegistration() { // Don't notify the listeners at this point; we might as well make ure the auth token is up // to date before letting them know. - boolean needRefresh = utils.isAuthTokenExpired(persistedFidEntry); + boolean needRefresh = utils.isAuthTokenExpired(persistedInstallationEntry); if (!needRefresh) { synchronized (lock) { needRefresh = shouldRefreshAuthToken; @@ -245,30 +248,31 @@ private final void doRegistration() { // Refresh Auth token if needed if (needRefresh) { - fetchAuthTokenFromServer(persistedFidEntry); - persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + fetchAuthTokenFromServer(persistedInstallationEntry); + persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); synchronized (lock) { shouldRefreshAuthToken = false; } } - triggerOnStateReached(persistedFidEntry); + triggerOnStateReached(persistedInstallationEntry); } catch (Exception e) { - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); - PersistedFidEntry errorFidEntry = - persistedFidEntry + PersistedInstallationEntry persistedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + PersistedInstallationEntry errorInstallationEntry = + persistedInstallationEntry .toBuilder() .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) .build(); - persistedFid.insertOrUpdatePersistedFidEntry(errorFidEntry); - triggerOnException(errorFidEntry, e); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(errorInstallationEntry); + triggerOnException(errorInstallationEntry, e); } } private void persistFid(String fid) throws FirebaseInstallationsException { boolean firstUpdateCacheResult = - persistedFid.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() .setFirebaseInstallationId(fid) .setRegistrationStatus(RegistrationStatus.UNREGISTERED) .build()); @@ -281,7 +285,7 @@ private void persistFid(String fid) throws FirebaseInstallationsException { } /** Registers the created Fid with FIS servers and update the shared prefs. */ - private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) + private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); @@ -289,12 +293,12 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) InstallationResponse installationResponse = serviceClient.createFirebaseInstallation( /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedFidEntry.getFirebaseInstallationId(), + /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), /*appId= */ getApplicationId()); - persistedFid.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() - .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(installationResponse.getFid()) .setRegistrationStatus(RegistrationStatus.REGISTERED) .setAuthToken(installationResponse.getAuthToken().getToken()) .setRefreshToken(installationResponse.getRefreshToken()) @@ -302,7 +306,7 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) .setTokenCreationEpochInSecs(creationTime) .build()); - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); } @@ -310,29 +314,29 @@ private Void registerAndSaveFid(PersistedFidEntry persistedFidEntry) } /** Calls the FIS servers to generate an auth token for this Firebase installation. */ - private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persistedFidEntry) - throws FirebaseInstallationsException { + private InstallationTokenResult fetchAuthTokenFromServer( + PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); InstallationTokenResult tokenResult = serviceClient.generateAuthToken( /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedFidEntry.getFirebaseInstallationId(), + /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*refreshToken= */ persistedFidEntry.getRefreshToken()); + /*refreshToken= */ persistedInstallationEntry.getRefreshToken()); - persistedFid.insertOrUpdatePersistedFidEntry( - PersistedFidEntry.builder() - .setFirebaseInstallationId(persistedFidEntry.getFirebaseInstallationId()) + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(persistedInstallationEntry.getFirebaseInstallationId()) .setRegistrationStatus(RegistrationStatus.REGISTERED) .setAuthToken(tokenResult.getToken()) - .setRefreshToken(persistedFidEntry.getRefreshToken()) + .setRefreshToken(persistedInstallationEntry.getRefreshToken()) .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) .setTokenCreationEpochInSecs(creationTime) .build()); return tokenResult; - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to generate auth token for a Firebase Installation.", FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); @@ -345,25 +349,26 @@ private InstallationTokenResult fetchAuthTokenFromServer(PersistedFidEntry persi */ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException { - PersistedFidEntry persistedFidEntry = persistedFid.readPersistedFidEntryValue(); + PersistedInstallationEntry persistedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); - if (persistedFidEntry.isRegistered()) { + if (persistedInstallationEntry.isRegistered()) { // Call the FIS servers to delete this Firebase Installation Id. try { serviceClient.deleteFirebaseInstallation( firebaseApp.getOptions().getApiKey(), - persistedFidEntry.getFirebaseInstallationId(), + persistedInstallationEntry.getFirebaseInstallationId(), firebaseApp.getOptions().getProjectId(), - persistedFidEntry.getRefreshToken()); + persistedInstallationEntry.getRefreshToken()); - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to delete a Firebase Installation.", FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); } } - persistedFid.clear(); + persistedInstallation.clear(); return null; } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index 008daadbe65..d0898aabd5a 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -15,7 +15,7 @@ package com.google.firebase.installations; import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallationEntry; class GetAuthTokenListener implements StateListener { private final Utils utils; @@ -29,16 +29,16 @@ public GetAuthTokenListener( @Override public boolean onStateReached( - PersistedFidEntry persistedFidEntry, boolean shouldRefreshAuthToken) { + PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken) { // AuthTokenListener state is reached when FID is registered and has a valid auth token - if (persistedFidEntry.isRegistered() - && !utils.isAuthTokenExpired(persistedFidEntry) + if (persistedInstallationEntry.isRegistered() + && !utils.isAuthTokenExpired(persistedInstallationEntry) && !shouldRefreshAuthToken) { resultTaskCompletionSource.setResult( InstallationTokenResult.builder() - .setToken(persistedFidEntry.getAuthToken()) - .setTokenExpirationTimestamp(persistedFidEntry.getExpiresInSecs()) - .setTokenCreationTimestamp(persistedFidEntry.getTokenCreationEpochInSecs()) + .setToken(persistedInstallationEntry.getAuthToken()) + .setTokenExpirationTimestamp(persistedInstallationEntry.getExpiresInSecs()) + .setTokenCreationTimestamp(persistedInstallationEntry.getTokenCreationEpochInSecs()) .build()); return true; } @@ -46,8 +46,9 @@ public boolean onStateReached( } @Override - public boolean onException(PersistedFidEntry persistedFidEntry, Exception exception) { - if (persistedFidEntry.isErrored()) { + public boolean onException( + PersistedInstallationEntry persistedInstallationEntry, Exception exception) { + if (persistedInstallationEntry.isErrored()) { resultTaskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java index 2e24b97cc07..38134cc6217 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java @@ -15,7 +15,7 @@ package com.google.firebase.installations; import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallationEntry; class GetIdListener implements StateListener { final TaskCompletionSource taskCompletionSource; @@ -25,17 +25,19 @@ public GetIdListener(TaskCompletionSource taskCompletionSource) { } @Override - public boolean onStateReached(PersistedFidEntry persistedFidEntry, boolean unused) { - if (persistedFidEntry.isUnregistered() || persistedFidEntry.isRegistered()) { - taskCompletionSource.trySetResult(persistedFidEntry.getFirebaseInstallationId()); + public boolean onStateReached( + PersistedInstallationEntry persistedInstallationEntry, boolean unused) { + if (persistedInstallationEntry.isUnregistered() || persistedInstallationEntry.isRegistered()) { + taskCompletionSource.trySetResult(persistedInstallationEntry.getFirebaseInstallationId()); return true; } return false; } @Override - public boolean onException(PersistedFidEntry persistedFidEntry, Exception exception) { - if (persistedFidEntry.isErrored()) { + public boolean onException( + PersistedInstallationEntry persistedInstallationEntry, Exception exception) { + if (persistedInstallationEntry.isErrored()) { taskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java index a9691f63d18..d9570835adf 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java @@ -14,18 +14,19 @@ package com.google.firebase.installations; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallationEntry; interface StateListener { /** - * Returns {@code true} if the defined {@link PersistedFidEntry} state is reached, {@code false} - * otherwise. + * Returns {@code true} if the defined {@link PersistedInstallationEntry} state is reached, {@code + * false} otherwise. */ - boolean onStateReached(PersistedFidEntry persistedFidEntry, boolean shouldRefreshAuthToken); + boolean onStateReached( + PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken); /** * Returns {@code true} if an exception is thrown while registering a Firebase Installation, * {@code false} otherwise. */ - boolean onException(PersistedFidEntry persistedFidEntry, Exception exception); + boolean onException(PersistedInstallationEntry persistedInstallationEntry, Exception exception); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index 1a3477e9894..03fd022befd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -16,7 +16,7 @@ import androidx.annotation.NonNull; import com.google.android.gms.common.util.Clock; -import com.google.firebase.installations.local.PersistedFidEntry; +import com.google.firebase.installations.local.PersistedInstallationEntry; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.UUID; @@ -52,8 +52,9 @@ class Utils { * Checks if the FIS Auth token is expired or going to expire in next 1 hour {@link * #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}. */ - public boolean isAuthTokenExpired(PersistedFidEntry persistedFidEntry) { - return persistedFidEntry.getTokenCreationEpochInSecs() + persistedFidEntry.getExpiresInSecs() + public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallationEntry) { + return persistedInstallationEntry.getTokenCreationEpochInSecs() + + persistedInstallationEntry.getExpiresInSecs() > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java similarity index 79% rename from firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java rename to firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index 808b277c467..aa47c16d8b1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFid.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -26,36 +26,36 @@ * A layer that locally persists a few Firebase Installation attributes on top the Firebase * Installation API. */ -public class PersistedFid { +public class PersistedInstallation { // Registration Status of each persisted fid entry // NOTE: never change the ordinal of the enum values because the enum values are stored in shared // prefs as their ordinal numbers. public enum RegistrationStatus { /** - * {@link PersistedFidEntry} default registration status. Next state: UNREGISTERED - A new FID - * is created and persisted locally before registering with FIS servers. + * {@link PersistedInstallationEntry} default registration status. Next state: UNREGISTERED - A + * new FID is created and persisted locally before registering with FIS servers. */ NOT_GENERATED, /** - * {@link PersistedFidEntry} is not synced with FIS servers. Next state: REGISTERED - If FID - * registration is successful. REGISTER_ERROR - If FID registration or refresh auth token + * {@link PersistedInstallationEntry} is not synced with FIS servers. Next state: REGISTERED - + * If FID registration is successful. REGISTER_ERROR - If FID registration or refresh auth token * failed. */ UNREGISTERED, /** - * {@link PersistedFidEntry} is synced to FIS servers. Next state: REGISTER_ERROR - If FID - * registration or refresh auth token failed. + * {@link PersistedInstallationEntry} is synced to FIS servers. Next state: REGISTER_ERROR - If + * FID registration or refresh auth token failed. */ REGISTERED, /** - * {@link PersistedFidEntry} is in error state when an exception is thrown while syncing with - * FIS server. Next state: UNREGISTERED - A new FID is created and persisted locally before - * registering with FIS servers. + * {@link PersistedInstallationEntry} is in error state when an exception is thrown while + * syncing with FIS server. Next state: UNREGISTERED - A new FID is created and persisted + * locally before registering with FIS servers. */ REGISTER_ERROR, } - private static final String SHARED_PREFS_NAME = "PersistedFid"; + private static final String SHARED_PREFS_NAME = "PersistedInstallation"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; @@ -78,7 +78,7 @@ public enum RegistrationStatus { private final String persistenceKey; - public PersistedFid(@NonNull FirebaseApp firebaseApp) { + public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path prefs = @@ -89,7 +89,7 @@ public PersistedFid(@NonNull FirebaseApp firebaseApp) { } @NonNull - public PersistedFidEntry readPersistedFidEntryValue() { + public PersistedInstallationEntry readPersistedInstallationEntryValue() { synchronized (prefs) { String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); @@ -100,9 +100,9 @@ public PersistedFidEntry readPersistedFidEntryValue() { long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { - return PersistedFidEntry.builder().build(); + return PersistedInstallationEntry.builder().build(); } - return PersistedFidEntry.builder() + return PersistedInstallationEntry.builder() .setFirebaseInstallationId(fid) .setRegistrationStatus(RegistrationStatus.values()[status]) .setAuthToken(authToken) @@ -114,7 +114,8 @@ public PersistedFidEntry readPersistedFidEntryValue() { } @NonNull - public boolean insertOrUpdatePersistedFidEntry(@NonNull PersistedFidEntry entryValue) { + public boolean insertOrUpdatePersistedInstallationEntry( + @NonNull PersistedInstallationEntry entryValue) { synchronized (prefs) { SharedPreferences.Editor editor = prefs.edit(); editor.putString( diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java similarity index 63% rename from firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java rename to firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java index 4019967cdb6..e7d66ae914f 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedFidEntry.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java @@ -19,17 +19,17 @@ import com.google.auto.value.AutoValue; /** - * This class represents a persisted fid entry in {@link PersistedFid}, which contains a few - * Firebase Installation attributes and the persisted status of this entry. + * This class represents a persisted fid entry in {@link PersistedInstallation}, which contains a + * few Firebase Installation attributes and the persisted status of this entry. */ @AutoValue -public abstract class PersistedFidEntry { +public abstract class PersistedInstallationEntry { @Nullable public abstract String getFirebaseInstallationId(); @NonNull - public abstract PersistedFid.RegistrationStatus getRegistrationStatus(); + public abstract PersistedInstallation.RegistrationStatus getRegistrationStatus(); @Nullable public abstract String getAuthToken(); @@ -42,30 +42,30 @@ public abstract class PersistedFidEntry { public abstract long getTokenCreationEpochInSecs(); public boolean isRegistered() { - return getRegistrationStatus() == PersistedFid.RegistrationStatus.REGISTERED; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTERED; } public boolean isErrored() { - return getRegistrationStatus() == PersistedFid.RegistrationStatus.REGISTER_ERROR; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTER_ERROR; } public boolean isUnregistered() { - return getRegistrationStatus() == PersistedFid.RegistrationStatus.UNREGISTERED; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.UNREGISTERED; } public boolean isNotGenerated() { - return getRegistrationStatus() == PersistedFid.RegistrationStatus.NOT_GENERATED; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED; } @NonNull public abstract Builder toBuilder(); - /** Returns a default Builder object to create an PersistedFidEntry object */ + /** Returns a default Builder object to create an PersistedInstallationEntry object */ @NonNull - public static PersistedFidEntry.Builder builder() { - return new AutoValue_PersistedFidEntry.Builder() + public static PersistedInstallationEntry.Builder builder() { + return new AutoValue_PersistedInstallationEntry.Builder() .setTokenCreationEpochInSecs(0) - .setRegistrationStatus(PersistedFid.RegistrationStatus.NOT_GENERATED) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.NOT_GENERATED) .setExpiresInSecs(0); } @@ -75,7 +75,8 @@ public abstract static class Builder { public abstract Builder setFirebaseInstallationId(@NonNull String value); @NonNull - public abstract Builder setRegistrationStatus(@NonNull PersistedFid.RegistrationStatus value); + public abstract Builder setRegistrationStatus( + @NonNull PersistedInstallation.RegistrationStatus value); @NonNull public abstract Builder setAuthToken(@Nullable String value); @@ -90,6 +91,6 @@ public abstract static class Builder { public abstract Builder setTokenCreationEpochInSecs(long value); @NonNull - public abstract PersistedFidEntry build(); + public abstract PersistedInstallationEntry build(); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index ddb43292471..6a1bafba6f5 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -25,7 +25,9 @@ import com.google.android.gms.common.util.AndroidUtilsLight; import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; +import com.google.firebase.FirebaseException; import com.google.firebase.installations.InstallationTokenResult; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; @@ -53,10 +55,8 @@ public class FirebaseInstallationServiceClient { private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; private static final String GZIP_CONTENT_ENCODING = "gzip"; - private static final String UNAUTHORIZED_ERROR_MESSAGE = - "The request did not have the required credentials."; private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; - private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error: %s"; private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; @@ -65,6 +65,9 @@ public class FirebaseInstallationServiceClient { private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s"); + private static final int MAX_RETRIES = 1; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + @VisibleForTesting static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp."; @@ -86,9 +89,10 @@ public FirebaseInstallationServiceClient(@NonNull Context context) { @NonNull public InstallationResponse createFirebaseInstallation( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); try { + int retryCount = 0; URL url = new URL( String.format( @@ -97,46 +101,36 @@ public InstallationResponse createFirebaseInstallation( FIREBASE_INSTALLATIONS_API_VERSION, resourceName, apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.setDoOutput(true); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); - httpsURLConnection.addRequestProperty( - X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); - - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readCreateResponse(httpsURLConnection); - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); - default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readCreateResponse(httpsURLConnection); + case 500: + retryCount++; + break; + default: + throw new FirebaseException(readErrorResponse(httpsURLConnection)); + } } + throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } @@ -163,7 +157,7 @@ public void deleteFirebaseInstallation( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = @@ -175,31 +169,19 @@ public void deleteFirebaseInstallation( resourceName, apiKey)); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); httpsURLConnection.setRequestMethod("DELETE"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); int httpResponseCode = httpsURLConnection.getResponseCode(); switch (httpResponseCode) { case 200: return; - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + throw new FirebaseException(readErrorResponse(httpsURLConnection)); } } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } @@ -218,10 +200,11 @@ public InstallationTokenResult generateAuthToken( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { + int retryCount = 0; URL url = new URL( String.format( @@ -230,46 +213,53 @@ public InstallationTokenResult generateAuthToken( FIREBASE_INSTALLATIONS_API_VERSION, resourceName, apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readGenerateAuthTokenResponse(httpsURLConnection); - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); - default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readGenerateAuthTokenResponse(httpsURLConnection); + case 500: + retryCount++; + break; + default: + throw new FirebaseException(readErrorResponse(httpsURLConnection)); + } } + throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } + + private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); + return httpsURLConnection; + } + // Read the response from the createFirebaseInstallation API. private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { - JsonReader reader = - new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); InstallationResponse.Builder builder = InstallationResponse.builder(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("name")) { - builder.setName(reader.nextString()); + builder.setUri(reader.nextString()); + } else if (name.equals("fid")) { + builder.setFid(reader.nextString()); } else if (name.equals("refreshToken")) { builder.setRefreshToken(reader.nextString()); } else if (name.equals("authToken")) { @@ -299,8 +289,7 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws // Read the response from the generateAuthToken FirebaseInstallation API. private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) throws IOException { - JsonReader reader = - new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); reader.beginObject(); while (reader.hasNext()) { @@ -318,6 +307,18 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection return builder.build(); } + // Read the error message from the response. + private String readErrorResponse(HttpsURLConnection conn) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), UTF_8)); + StringBuilder response = new StringBuilder(); + for (String input = reader.readLine(); input != null; input = reader.readLine()) { + response.append(input).append('\n'); + } + return String.format( + "The server responded with an error. HTTP response: [%d %s %s]", + conn.getResponseCode(), conn.getResponseMessage(), response); + } + /** Gets the Android package's SHA-1 fingerprint. */ private String getFingerprintHashForPackage() { byte[] hash; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java deleted file mode 100644 index b9be3727d4d..00000000000 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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.installations.remote; - -import androidx.annotation.NonNull; -import com.google.firebase.FirebaseException; - -/** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ -public class FirebaseInstallationServiceException extends FirebaseException { - public enum Status { - SERVER_ERROR, - - NETWORK_ERROR, - - UNAUTHORIZED - } - - @NonNull private final Status status; - - public FirebaseInstallationServiceException(@NonNull Status status) { - this.status = status; - } - - public FirebaseInstallationServiceException(@NonNull String message, @NonNull Status status) { - super(message); - this.status = status; - } - - public FirebaseInstallationServiceException( - @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { - super(message, cause); - this.status = status; - } - - /** - * Gets the status status for the operation that failed. - * - * @return the status for the FirebaseInstallationServiceException - */ - @NonNull - public Status getStatus() { - return status; - } -} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index 2022a498fe1..c0c04a33be9 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -22,7 +22,10 @@ public abstract class InstallationResponse { @NonNull - public abstract String getName(); + public abstract String getUri(); + + @NonNull + public abstract String getFid(); @NonNull public abstract String getRefreshToken(); @@ -42,7 +45,10 @@ public static InstallationResponse.Builder builder() { @AutoValue.Builder public abstract static class Builder { @NonNull - public abstract Builder setName(@NonNull String value); + public abstract Builder setUri(@NonNull String value); + + @NonNull + public abstract Builder setFid(@NonNull String value); @NonNull public abstract Builder setRefreshToken(@NonNull String value); From 42e442fb81c48b5868dd88c2836cc969ad388f15 Mon Sep 17 00:00:00 2001 From: Ankita Date: Wed, 16 Oct 2019 10:38:51 -0700 Subject: [PATCH 60/74] FIS error handling (#911) 1. Mark PersistedInstallationEntry status UNREGISTERED incase of 500 errors 2. Store detailed 4xx server exceptions in the local storage. In the succeeding getId calls,return these exceptions to the clients --- firebase-installations/api.txt | 17 +++++-- ...FirebaseInstallationsInstrumentedTest.java | 27 ++++++++++++ .../FisAndroidTestConstants.java | 5 +++ .../installations/FirebaseInstallations.java | 30 ++++++++----- .../google/firebase/installations/Utils.java | 7 +-- .../local/PersistedInstallationEntry.java | 6 +++ .../FirebaseInstallationServiceClient.java | 44 +++++++++++-------- .../remote/InstallationResponse.java | 22 ++++++++-- 8 files changed, 119 insertions(+), 39 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 4e676ce4849..e344cd2e484 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -45,6 +45,7 @@ package com.google.firebase.installations.local { method @Nullable public abstract String getAuthToken(); method public abstract long getExpiresInSecs(); method @Nullable public abstract String getFirebaseInstallationId(); + method @Nullable public abstract String getFisError(); method @Nullable public abstract String getRefreshToken(); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus getRegistrationStatus(); method public abstract long getTokenCreationEpochInSecs(); @@ -61,6 +62,7 @@ package com.google.firebase.installations.local { method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setAuthToken(@Nullable String); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setExpiresInSecs(long); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setFirebaseInstallationId(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setFisError(@Nullable String); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setRefreshToken(@Nullable String); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setRegistrationStatus(@NonNull com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder setTokenCreationEpochInSecs(long); @@ -80,10 +82,11 @@ package com.google.firebase.installations.remote { public abstract class InstallationResponse { ctor public InstallationResponse(); method @NonNull public static com.google.firebase.installations.remote.InstallationResponse.Builder builder(); - method @NonNull public abstract InstallationTokenResult getAuthToken(); - method @NonNull public abstract String getFid(); - method @NonNull public abstract String getRefreshToken(); - method @NonNull public abstract String getUri(); + method @Nullable public abstract InstallationTokenResult getAuthToken(); + method @Nullable public abstract String getFid(); + method @Nullable public abstract String getRefreshToken(); + method @Nullable public abstract com.google.firebase.installations.remote.InstallationResponse.ResponseCode getResponseCode(); + method @Nullable public abstract String getUri(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder toBuilder(); } @@ -93,8 +96,14 @@ package com.google.firebase.installations.remote { method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull InstallationTokenResult); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setFid(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setRefreshToken(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setResponseCode(@NonNull com.google.firebase.installations.remote.InstallationResponse.ResponseCode); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setUri(@NonNull String); } + public enum InstallationResponse.ResponseCode { + enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode OK; + enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode SERVER_ERROR; + } + } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index d64d34ffc72..caa4eea58ff 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_INSTALLATION_ENTRY; import static com.google.firebase.installations.FisAndroidTestConstants.INVALID_TEST_FID; +import static com.google.firebase.installations.FisAndroidTestConstants.SERVER_ERROR_INSTALLATION_RESPONSE; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; @@ -291,6 +292,32 @@ public void testGetId_PersistedInstallationOk_BackendError() throws Exception { assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } + @Test + public void testGetId_ServerError_UnregisteredFID() throws Exception { + // Mocking server error on FIS createFirebaseInstallation, returns empty InstallationResponse + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(SERVER_ERROR_INSTALLATION_RESPONSE); + + FirebaseInstallations firebaseInstallations = + new FirebaseInstallations( + executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + + Tasks.await(firebaseInstallations.getId()); + + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); + } + @Test public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedException { FirebaseInstallations firebaseInstallations = diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index d94cdcc214c..1f17b79aee9 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -16,6 +16,7 @@ import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; @@ -55,6 +56,7 @@ public final class FisAndroidTestConstants { .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build()) + .setResponseCode(ResponseCode.OK) .build(); public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = @@ -63,4 +65,7 @@ public final class FisAndroidTestConstants { .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build(); + + public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE = + InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 709b78637a8..a29fa0e7d9e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -29,6 +29,7 @@ import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -218,12 +219,18 @@ private final void doRegistration() { persistedInstallation.readPersistedInstallationEntryValue(); // New FID needs to be created - if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { + if (persistedInstallationEntry.isNotGenerated()) { String fid = utils.createRandomFid(); persistFid(fid); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); } + if (persistedInstallationEntry.isErrored()) { + throw new FirebaseInstallationsException( + persistedInstallationEntry.getFisError(), + FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + } + triggerOnStateReached(persistedInstallationEntry); // FID needs to be registered @@ -262,6 +269,7 @@ private final void doRegistration() { PersistedInstallationEntry errorInstallationEntry = persistedInstallationEntry .toBuilder() + .setFisError(e.getMessage()) .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) .build(); persistedInstallation.insertOrUpdatePersistedInstallationEntry(errorInstallationEntry); @@ -296,15 +304,17 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), /*appId= */ getApplicationId()); - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(installationResponse.getFid()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(installationResponse.getAuthToken().getToken()) - .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); + if (installationResponse.getResponseCode() == ResponseCode.OK) { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(installationResponse.getFid()) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(installationResponse.getAuthToken().getToken()) + .setRefreshToken(installationResponse.getRefreshToken()) + .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + } } catch (FirebaseException exception) { throw new FirebaseInstallationsException( diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index 03fd022befd..d367ed6e230 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -53,9 +53,10 @@ class Utils { * #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}. */ public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallationEntry) { - return persistedInstallationEntry.getTokenCreationEpochInSecs() - + persistedInstallationEntry.getExpiresInSecs() - > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + return persistedInstallationEntry.isRegistered() + && persistedInstallationEntry.getTokenCreationEpochInSecs() + + persistedInstallationEntry.getExpiresInSecs() + > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; } /** Returns current time in seconds. */ diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java index e7d66ae914f..aa2ffee2e58 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java @@ -41,6 +41,9 @@ public abstract class PersistedInstallationEntry { public abstract long getTokenCreationEpochInSecs(); + @Nullable + public abstract String getFisError(); + public boolean isRegistered() { return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTERED; } @@ -90,6 +93,9 @@ public abstract Builder setRegistrationStatus( @NonNull public abstract Builder setTokenCreationEpochInSecs(long value); + @NonNull + public abstract Builder setFisError(@Nullable String value); + @NonNull public abstract PersistedInstallationEntry build(); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 6a1bafba6f5..65a26534623 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -27,6 +27,7 @@ import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; import com.google.firebase.installations.InstallationTokenResult; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -118,17 +119,21 @@ public InstallationResponse createFirebaseInstallation( } int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readCreateResponse(httpsURLConnection); - case 500: - retryCount++; - break; - default: - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + + if (httpResponseCode == 200) { + return readCreateResponse(httpsURLConnection); + } + // Usually the FIS server recovers from errors: retry one time before giving up. + if (httpResponseCode >= 500 && httpResponseCode < 600) { + retryCount++; + continue; } + + // Unrecoverable server response or unknown error + throw new FirebaseException(readErrorResponse(httpsURLConnection)); } - throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); + // Return empty installation response with SERVER_ERROR response code after max retries + return InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); } catch (IOException e) { throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } @@ -219,15 +224,18 @@ public InstallationTokenResult generateAuthToken( httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readGenerateAuthTokenResponse(httpsURLConnection); - case 500: - retryCount++; - break; - default: - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + + if (httpResponseCode == 200) { + return readGenerateAuthTokenResponse(httpsURLConnection); + } + // Usually the FIS server recovers from errors: retry one time before giving up. + if (httpResponseCode >= 500 && httpResponseCode < 600) { + retryCount++; + continue; } + + // Unrecoverable server response or unknown error + throw new FirebaseException(readErrorResponse(httpsURLConnection)); } throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); } catch (IOException e) { @@ -283,7 +291,7 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws } reader.endObject(); - return builder.build(); + return builder.setResponseCode(ResponseCode.OK).build(); } // Read the response from the generateAuthToken FirebaseInstallation API. diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index c0c04a33be9..8bea66ff2e4 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -15,24 +15,35 @@ package com.google.firebase.installations.remote; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; import com.google.firebase.installations.InstallationTokenResult; @AutoValue public abstract class InstallationResponse { - @NonNull + public enum ResponseCode { + // Returned on success + OK, + // An error occurred on the server while processing this request(temporary) + SERVER_ERROR + } + + @Nullable public abstract String getUri(); - @NonNull + @Nullable public abstract String getFid(); - @NonNull + @Nullable public abstract String getRefreshToken(); - @NonNull + @Nullable public abstract InstallationTokenResult getAuthToken(); + @Nullable + public abstract ResponseCode getResponseCode(); + @NonNull public abstract Builder toBuilder(); @@ -56,6 +67,9 @@ public abstract static class Builder { @NonNull public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + @NonNull + public abstract Builder setResponseCode(@NonNull ResponseCode value); + @NonNull public abstract InstallationResponse build(); } From 98b2aac6be09f3cb9461123e3e0d3e80c7e23f93 Mon Sep 17 00:00:00 2001 From: Ankita Date: Fri, 25 Oct 2019 11:43:09 -0700 Subject: [PATCH 61/74] Reuse existing Iid as Fid (#924) * Add FisError to the persisted installation entry. (#931) --- firebase-installations/api.txt | 5 + ...FirebaseInstallationsInstrumentedTest.java | 145 ++++++++++++------ .../FisAndroidTestConstants.java | 16 ++ .../installations/FirebaseInstallations.java | 27 +++- .../installations/local/IidStore.java | 135 ++++++++++++++++ .../local/PersistedInstallation.java | 7 +- 6 files changed, 282 insertions(+), 53 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index e344cd2e484..1a7f890c6d3 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -25,6 +25,11 @@ package com.google.firebase.installations { package com.google.firebase.installations.local { + public class IidStore { + ctor public IidStore(); + method @Nullable public String readIid(); + } + public class PersistedInstallation { ctor public PersistedInstallation(@NonNull FirebaseApp); method @NonNull public boolean clear(); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index caa4eea58ff..dc569af259c 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -28,7 +28,9 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_TOKEN_RESULT; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; @@ -52,6 +54,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; @@ -88,6 +91,7 @@ public class FirebaseInstallationsInstrumentedTest { @Mock private Utils mockUtils; @Mock private PersistedInstallation mockPersistedInstallation; @Mock private FirebaseInstallationServiceClient mockClient; + @Mock private IidStore mockIidStore; private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder() @@ -99,6 +103,16 @@ public class FirebaseInstallationsInstrumentedTest { .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); + private static final PersistedInstallationEntry REGISTERED_IID_ENTRY = + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_INSTANCE_ID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) + .build(); + private static final PersistedInstallationEntry EXPIRED_AUTH_TOKEN_ENTRY = PersistedInstallationEntry.builder() .setFirebaseInstallationId(TEST_FID_1) @@ -189,12 +203,20 @@ public void cleanUp() throws Exception { persistedInstallation.clear(); } + private FirebaseInstallations getFirebaseInstallations() { + return new FirebaseInstallations( + executor, + firebaseApp, + backendClientReturnsOk, + persistedInstallation, + mockUtils, + mockIidStore); + } + @Test public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(false); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. assertWithMessage("getId Task failed.") @@ -204,7 +226,8 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_FID_1); - // Waiting for Task that registers FID on the FIS Servers + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. executor.awaitTermination(500, TimeUnit.MILLISECONDS); PersistedInstallationEntry updatedInstallationEntry = @@ -213,15 +236,39 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + @Test + public void testGetId_migrateIid_successful() throws Exception { + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(backendClientReturnsOk.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + + // No exception, means success. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isNotEmpty(); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + } + @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getId multiple times Task task1 = firebaseInstallations.getId(); @@ -249,9 +296,7 @@ public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. assertWithMessage("getId Task failed.") @@ -275,7 +320,12 @@ public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { public void testGetId_PersistedInstallationOk_BackendError() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + persistedInstallation, + mockUtils, + mockIidStore); Tasks.await(firebaseInstallations.getId()); @@ -299,9 +349,7 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception { anyString(), anyString(), anyString(), anyString())) .thenReturn(SERVER_ERROR_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getId()); @@ -326,7 +374,8 @@ public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedE firebaseApp, backendClientReturnsOk, persistedInstallationReturnsError, - mockUtils); + mockUtils, + mockIidStore); // Expect exception try { @@ -355,7 +404,7 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils); + executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); Tasks.await(firebaseInstallations.getId()); @@ -387,7 +436,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils); + executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) @@ -412,9 +461,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) @@ -439,9 +486,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -459,7 +504,8 @@ public void testGetAuthToken_PersistedInstallationError_failure() throws Excepti firebaseApp, backendClientReturnsOk, persistedInstallationReturnsError, - mockUtils); + mockUtils, + mockIidStore); // Expect exception try { @@ -485,7 +531,12 @@ public void testGetAuthToken_fidExists_successful() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, mockPersistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsOk, + mockPersistedInstallation, + mockUtils, + mockIidStore); InstallationTokenResult installationTokenResult = Tasks.await( @@ -504,9 +555,7 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = Tasks.await( @@ -526,9 +575,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = Tasks.await( @@ -552,7 +599,12 @@ public void testGetAuthToken_serverError_failure() throws Exception { FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, mockPersistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + mockPersistedInstallation, + mockUtils, + mockIidStore); // Expect exception try { @@ -579,9 +631,7 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -630,9 +680,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th .generateAuthToken(anyString(), anyString(), anyString(), anyString()); when(mockUtils.isAuthTokenExpired(any())).thenReturn(false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getAuthToken multiple times with FORCE_REFRESH option. Task task1 = @@ -659,9 +707,7 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -676,9 +722,7 @@ public void testDelete_registeredFID_successful() throws Exception { public void testDelete_unregisteredFID_successful() throws Exception { // Update local storage with a unregistered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -691,9 +735,7 @@ public void testDelete_unregisteredFID_successful() throws Exception { @Test public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsOk, persistedInstallation, mockUtils); + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.delete()); @@ -710,7 +752,12 @@ public void testDelete_serverError_failure() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( - executor, firebaseApp, backendClientReturnsError, persistedInstallation, mockUtils); + executor, + firebaseApp, + backendClientReturnsError, + persistedInstallation, + mockUtils, + mockIidStore); // Expect exception try { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 1f17b79aee9..f0cefaa133e 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -43,6 +43,8 @@ public final class FisAndroidTestConstants { public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; + public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; + public static final PersistedInstallationEntry DEFAULT_PERSISTED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder().build(); public static final InstallationResponse TEST_INSTALLATION_RESPONSE = @@ -59,6 +61,20 @@ public final class FisAndroidTestConstants { .setResponseCode(ResponseCode.OK) .build(); + public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) + .setFid(TEST_INSTANCE_ID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + InstallationTokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = InstallationTokenResult.builder() .setToken(TEST_AUTH_TOKEN_2) diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index a29fa0e7d9e..8a9574d09ee 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -24,6 +24,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; @@ -56,6 +57,7 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final PersistedInstallation persistedInstallation; private final ExecutorService executor; private final Utils utils; + private final IidStore iidStore; private final Object lock = new Object(); @GuardedBy("lock") @@ -71,7 +73,8 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { firebaseApp, new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), new PersistedInstallation(firebaseApp), - new Utils(DefaultClock.getInstance())); + new Utils(DefaultClock.getInstance()), + new IidStore()); } FirebaseInstallations( @@ -79,12 +82,14 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { FirebaseApp firebaseApp, FirebaseInstallationServiceClient serviceClient, PersistedInstallation persistedInstallation, - Utils utils) { + Utils utils, + IidStore iidStore) { this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; this.persistedInstallation = persistedInstallation; this.utils = utils; + this.iidStore = iidStore; } /** @@ -220,7 +225,10 @@ private final void doRegistration() { // New FID needs to be created if (persistedInstallationEntry.isNotGenerated()) { - String fid = utils.createRandomFid(); + + // For a default firebase installation read the existing iid. For other custom firebase + // installations create a new fid + String fid = readExistingIidOrCreateFid(); persistFid(fid); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); } @@ -277,6 +285,19 @@ private final void doRegistration() { } } + private String readExistingIidOrCreateFid() { + // Check if this firebase app is the default (first initialized) instance + if (!firebaseApp.equals(FirebaseApp.getInstance())) { + return utils.createRandomFid(); + } + // For a default firebase installation, read the existing iid from shared prefs + String fid = iidStore.readIid(); + if (fid == null) { + fid = utils.createRandomFid(); + } + return fid; + } + private void persistFid(String fid) throws FirebaseInstallationsException { boolean firstUpdateCacheResult = persistedInstallation.insertOrUpdatePersistedInstallationEntry( diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java new file mode 100644 index 00000000000..4e8366df7bf --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java @@ -0,0 +1,135 @@ +// 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.installations.local; + +import static android.content.ContentValues.TAG; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.FirebaseApp; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Read existing iid only for default (first initialized) instance of this firebase application.* + */ +public class IidStore { + private static final String IID_SHARED_PREFS_NAME = "com.google.android.gms.appid"; + private static final String STORE_KEY_PUB = "|S||P|"; + private static final String STORE_KEY_ID = "|S|id"; + + @GuardedBy("iidPrefs") + private final SharedPreferences iidPrefs; + + public IidStore() { + // Different FirebaseApp in the same Android application should have the same application + // context and same dir path. We only read existing Iids for the default firebase application. + iidPrefs = + FirebaseApp.getInstance() + .getApplicationContext() + .getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE); + } + + @Nullable + public String readIid() { + synchronized (iidPrefs) { + // Background: Some versions of the IID-SDK store the Instance-ID in local storage, + // others only store the App-Instance's Public-Key that can be used to calculate the + // Instance-ID. + + // If such a version was used by this App-Instance, we can directly read the existing + // Instance-ID from storage and return it + String id = readInstanceIdFromLocalStorage(); + + if (id != null) { + return id; + } + + // If this App-Instance did not store the Instance-ID in local storage, we may be able to find + // its Public-Key in order to calculate the App-Instance's Instance-ID. + return readPublicKeyFromLocalStorageAndCalculateInstanceId(); + } + } + + @Nullable + private String readInstanceIdFromLocalStorage() { + synchronized (iidPrefs) { + return iidPrefs.getString(STORE_KEY_ID, /* defaultValue= */ null); + } + } + + @Nullable + private String readPublicKeyFromLocalStorageAndCalculateInstanceId() { + synchronized (iidPrefs) { + String base64PublicKey = iidPrefs.getString(STORE_KEY_PUB, /* defaultValue= */ null); + if (base64PublicKey == null) { + return null; + } + + PublicKey publicKey = parseKey(base64PublicKey); + if (publicKey == null) { + return null; + } + + return getIdFromPublicKey(publicKey); + } + } + + @Nullable + private static String getIdFromPublicKey(@NonNull PublicKey publicKey) { + // The ID is the sha of the public key truncated to 60 bit, with first 4 bits switched to + // 0x9 and base64 encoded + // This allows the id to be used internally for legacy systems and differentiate from + // old android ids and gcm ids + + byte[] derPub = publicKey.getEncoded(); + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + + byte[] digest = md.digest(derPub); + int b0 = digest[0]; + b0 = 0x70 + (0xF & b0); + digest[0] = (byte) (b0 & 0xFF); + return Base64.encodeToString( + digest, 0, 8, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, "Unexpected error, device missing required algorithms"); + } + return null; + } + + /** Parse the public key from stored data. */ + @Nullable + private PublicKey parseKey(String base64PublicKey) { + byte[] publicKeyBytes; + try { + publicKeyBytes = Base64.decode(base64PublicKey, Base64.URL_SAFE); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + } catch (IllegalArgumentException | InvalidKeySpecException | NoSuchAlgorithmException e) { + Log.w(TAG, "Invalid key stored " + e); + } + return null; + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index aa47c16d8b1..fdee274945d 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -63,6 +63,7 @@ public enum RegistrationStatus { private static final String TOKEN_CREATION_TIME_IN_SECONDS_KEY = "TokenCreationEpochInSecs"; private static final String EXPIRES_IN_SECONDS_KEY = "ExpiresInSecs"; private static final String PERSISTED_STATUS_KEY = "Status"; + private static final String FIS_ERROR_KEY = "FisError"; private static final List FID_PREF_KEYS = Arrays.asList( @@ -71,7 +72,8 @@ public enum RegistrationStatus { REFRESH_TOKEN_KEY, TOKEN_CREATION_TIME_IN_SECONDS_KEY, EXPIRES_IN_SECONDS_KEY, - PERSISTED_STATUS_KEY); + PERSISTED_STATUS_KEY, + FIS_ERROR_KEY); @GuardedBy("prefs") private final SharedPreferences prefs; @@ -98,6 +100,7 @@ public PersistedInstallationEntry readPersistedInstallationEntryValue() { long tokenCreationTime = prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); + String fisError = prefs.getString(getSharedPreferencesKey(FIS_ERROR_KEY), null); if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { return PersistedInstallationEntry.builder().build(); @@ -109,6 +112,7 @@ public PersistedInstallationEntry readPersistedInstallationEntryValue() { .setRefreshToken(refreshToken) .setTokenCreationEpochInSecs(tokenCreationTime) .setExpiresInSecs(expiresIn) + .setFisError(fisError) .build(); } } @@ -131,6 +135,7 @@ public boolean insertOrUpdatePersistedInstallationEntry( entryValue.getTokenCreationEpochInSecs()); editor.putLong( getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); + editor.putString(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); return editor.commit(); } } From 0c1ef2cef47edd4fa2369c998474b11a98fe60d3 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 21 Nov 2019 10:45:33 -0800 Subject: [PATCH 62/74] touch file (#993) 1 line change on comment --- .../google/firebase/installations/FisAndroidTestConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index f0cefaa133e..98fd26b0542 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -20,7 +20,7 @@ public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; - /** Invalid FID. */ + // Invalid FID. public static final String INVALID_TEST_FID = "invalid"; public static final String TEST_PROJECT_ID = "777777777777"; From 101ee18b50fc85e7d300041b2daa782989297ac7 Mon Sep 17 00:00:00 2001 From: Ankita Date: Thu, 21 Nov 2019 11:12:53 -0800 Subject: [PATCH 63/74] getAuthToken error handling for 401 & 404 response code (#961) * getAuthToken error handling for 401 & 404 response code: - Clear the local storage & throw an exception to call getId() resulting in recreation of fid --- firebase-installations/api.txt | 31 ++++++- ...FirebaseInstallationsInstrumentedTest.java | 83 ++++++++++++++----- .../FisAndroidTestConstants.java | 13 ++- .../installations/FirebaseInstallations.java | 43 +++++++--- .../FirebaseInstallationsException.java | 7 +- .../installations/GetAuthTokenListener.java | 2 +- .../google/firebase/installations/Utils.java | 2 +- .../FirebaseInstallationServiceClient.java | 29 ++++--- .../remote/InstallationResponse.java | 5 +- .../installations/remote/TokenResult.java | 73 ++++++++++++++++ 10 files changed, 224 insertions(+), 64 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 1a7f890c6d3..06028ad7b5d 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -17,6 +17,7 @@ package com.google.firebase.installations { } public enum FirebaseInstallationsException.Status { + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status AUTHENTICATION_ERROR; enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status CLIENT_ERROR; enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status SDK_INTERNAL_ERROR; } @@ -81,13 +82,13 @@ package com.google.firebase.installations.remote { ctor public FirebaseInstallationServiceClient(@NonNull Context); method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); - method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); } public abstract class InstallationResponse { ctor public InstallationResponse(); method @NonNull public static com.google.firebase.installations.remote.InstallationResponse.Builder builder(); - method @Nullable public abstract InstallationTokenResult getAuthToken(); + method @Nullable public abstract com.google.firebase.installations.remote.TokenResult getAuthToken(); method @Nullable public abstract String getFid(); method @Nullable public abstract String getRefreshToken(); method @Nullable public abstract com.google.firebase.installations.remote.InstallationResponse.ResponseCode getResponseCode(); @@ -98,7 +99,7 @@ package com.google.firebase.installations.remote { public abstract static class InstallationResponse.Builder { ctor public InstallationResponse.Builder(); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse build(); - method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull InstallationTokenResult); + method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setAuthToken(@NonNull com.google.firebase.installations.remote.TokenResult); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setFid(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setRefreshToken(@NonNull String); method @NonNull public abstract com.google.firebase.installations.remote.InstallationResponse.Builder setResponseCode(@NonNull com.google.firebase.installations.remote.InstallationResponse.ResponseCode); @@ -110,5 +111,29 @@ package com.google.firebase.installations.remote { enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode SERVER_ERROR; } + public abstract class TokenResult { + ctor public TokenResult(); + method @NonNull public static com.google.firebase.installations.remote.TokenResult.Builder builder(); + method @Nullable public abstract com.google.firebase.installations.remote.TokenResult.ResponseCode getResponseCode(); + method @Nullable public abstract String getToken(); + method @NonNull public abstract long getTokenExpirationTimestamp(); + method public boolean isSuccessful(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder toBuilder(); + } + + public abstract static class TokenResult.Builder { + ctor public TokenResult.Builder(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult build(); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setResponseCode(@NonNull com.google.firebase.installations.remote.TokenResult.ResponseCode); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setToken(@NonNull String); + method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder setTokenExpirationTimestamp(long); + } + + public enum TokenResult.ResponseCode { + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode FID_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode OK; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode REFRESH_TOKEN_ERROR; + } + } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index dc569af259c..26ae5acd6c8 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -29,12 +29,12 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_TOKEN_RESULT; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; +import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -59,6 +59,7 @@ import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.TokenResult; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -174,7 +175,7 @@ public void setUp() throws FirebaseException { // Mocks successful auth token generation when(backendClientReturnsOk.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_TOKEN_RESULT); + .thenReturn(TEST_TOKEN_RESULT); when(persistedInstallationReturnsError.insertOrUpdatePersistedInstallationEntry(any())) .thenReturn(false); @@ -215,7 +216,7 @@ private FirebaseInstallations getFirebaseInstallations() { @Test public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. @@ -239,7 +240,7 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); @@ -264,7 +265,7 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); when(backendClientReturnsOk.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); @@ -295,7 +296,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // No exception, means success. @@ -400,7 +401,7 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E invocation -> { throw new InterruptedException(); }); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -432,7 +433,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws invocation -> { throw new InterruptedException(); }); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -459,7 +460,7 @@ public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { // Update local storage with installation entry that has auth token expired. persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -485,7 +486,7 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -497,7 +498,7 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { @Test public void testGetAuthToken_PersistedInstallationError_failure() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( executor, @@ -527,7 +528,7 @@ public void testGetAuthToken_PersistedInstallationError_failure() throws Excepti public void testGetAuthToken_fidExists_successful() throws Exception { when(mockPersistedInstallation.readPersistedInstallationEntryValue()) .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -552,8 +553,8 @@ public void testGetAuthToken_fidExists_successful() throws Exception { @Test public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -573,7 +574,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc // Update local storage with a unregistered installation entry to validate that getAuthToken // calls getId to ensure FID registration and returns a valid auth token. persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -588,6 +589,42 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); } + @Test + public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exception { + // Update local storage with an expired installation entry to ensure that generate auth token + // is called. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Mocks error during auth token generation + when(backendClientReturnsOk.generateAuthToken( + anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build()); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)) + .thenReturn(/* isExpired*/ true, /*isValid*/ false); + + FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + + // Expect exception + try { + Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); + fail("getAuthToken() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(FirebaseInstallationsException.Status.AUTHENTICATION_ERROR); + } + + verify(backendClientReturnsOk, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + } + @Test public void testGetAuthToken_serverError_failure() throws Exception { when(mockPersistedInstallation.readPersistedInstallationEntryValue()) @@ -595,7 +632,7 @@ public void testGetAuthToken_serverError_failure() throws Exception { when(backendClientReturnsError.generateAuthToken( anyString(), anyString(), anyString(), anyString())) .thenThrow(new FirebaseException("Server Error")); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = new FirebaseInstallations( @@ -628,8 +665,8 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(false); + when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); @@ -662,23 +699,23 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th AdditionalAnswers.answersWithDelay( 500, (unused) -> - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_3) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build())) .doAnswer( AdditionalAnswers.answersWithDelay( 500, (unused) -> - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_4) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build())) .when(backendClientReturnsOk) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - when(mockUtils.isAuthTokenExpired(any())).thenReturn(false); + when(mockUtils.isAuthTokenExpired(any())).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 98fd26b0542..eb4a0f47f85 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -17,6 +17,7 @@ import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; @@ -53,10 +54,9 @@ public final class FisAndroidTestConstants { .setFid(TEST_FID_1) .setRefreshToken(TEST_REFRESH_TOKEN) .setAuthToken( - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build()) .setResponseCode(ResponseCode.OK) .build(); @@ -67,19 +67,18 @@ public final class FisAndroidTestConstants { .setFid(TEST_INSTANCE_ID_1) .setRefreshToken(TEST_REFRESH_TOKEN) .setAuthToken( - InstallationTokenResult.builder() + TokenResult.builder() .setToken(TEST_AUTH_TOKEN) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) .build()) .setResponseCode(ResponseCode.OK) .build(); - public static final InstallationTokenResult TEST_INSTALLATION_TOKEN_RESULT = - InstallationTokenResult.builder() + public static final TokenResult TEST_TOKEN_RESULT = + TokenResult.builder() .setToken(TEST_AUTH_TOKEN_2) .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setTokenCreationTimestamp(TEST_CREATION_TIMESTAMP_1) + .setResponseCode(TokenResult.ResponseCode.OK) .build(); public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE = diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 8a9574d09ee..b0aff260fd1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -31,6 +31,7 @@ import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -261,15 +262,28 @@ private final void doRegistration() { } } + TokenResult tokenResult = null; // Refresh Auth token if needed if (needRefresh) { - fetchAuthTokenFromServer(persistedInstallationEntry); + tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); synchronized (lock) { shouldRefreshAuthToken = false; } } + // If tokenResult is not null and is not successful, it was cleared due to authentication + // error during auth token generation. + if (tokenResult != null && !tokenResult.isSuccessful()) { + triggerOnException( + persistedInstallationEntry, + new FirebaseInstallationsException( + "Failed to generate auth token for this Firebase Installation. Call getId() " + + "to recreate a new Fid and a valid auth token.", + FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); + return; + } + triggerOnStateReached(persistedInstallationEntry); } catch (Exception e) { PersistedInstallationEntry persistedInstallationEntry = @@ -345,28 +359,31 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation } /** Calls the FIS servers to generate an auth token for this Firebase installation. */ - private InstallationTokenResult fetchAuthTokenFromServer( + private TokenResult fetchAuthTokenFromServer( PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { try { long creationTime = utils.currentTimeInSecs(); - InstallationTokenResult tokenResult = + TokenResult tokenResult = serviceClient.generateAuthToken( /*apiKey= */ firebaseApp.getOptions().getApiKey(), /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), /*refreshToken= */ persistedInstallationEntry.getRefreshToken()); - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(persistedInstallationEntry.getFirebaseInstallationId()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(tokenResult.getToken()) - .setRefreshToken(persistedInstallationEntry.getRefreshToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); - + if (tokenResult.isSuccessful()) { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + persistedInstallationEntry + .toBuilder() + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(tokenResult.getToken()) + .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) + .setTokenCreationEpochInSecs(creationTime) + .build()); + } else { + persistedInstallation.clear(); + } return tokenResult; + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to generate auth token for a Firebase Installation.", diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index fd627a27911..4309a782ffd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -19,12 +19,13 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ public class FirebaseInstallationsException extends FirebaseException { - - // TODO(ankitagj): Improve clear exception handling. + // TODO(ankitagj): Improve exception handling and java doc public enum Status { SDK_INTERNAL_ERROR, - CLIENT_ERROR + CLIENT_ERROR, + + AUTHENTICATION_ERROR } @NonNull private final Status status; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index d0898aabd5a..bc36a20bac6 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -48,7 +48,7 @@ public boolean onStateReached( @Override public boolean onException( PersistedInstallationEntry persistedInstallationEntry, Exception exception) { - if (persistedInstallationEntry.isErrored()) { + if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { resultTaskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index d367ed6e230..22bf2576ea5 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -56,7 +56,7 @@ public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallati return persistedInstallationEntry.isRegistered() && persistedInstallationEntry.getTokenCreationEpochInSecs() + persistedInstallationEntry.getExpiresInSecs() - > currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + < currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; } /** Returns current time in seconds. */ diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index 65a26534623..bc5947bbe7c 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -26,7 +26,6 @@ import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; -import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import java.io.BufferedReader; import java.io.IOException; @@ -200,7 +199,7 @@ public void deleteFirebaseInstallation( * @param refreshToken a token used to authenticate FIS requests */ @NonNull - public InstallationTokenResult generateAuthToken( + public TokenResult generateAuthToken( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @@ -228,6 +227,17 @@ public InstallationTokenResult generateAuthToken( if (httpResponseCode == 200) { return readGenerateAuthTokenResponse(httpsURLConnection); } + + if (httpResponseCode == 401) { + return TokenResult.builder() + .setResponseCode(TokenResult.ResponseCode.REFRESH_TOKEN_ERROR) + .build(); + } + + if (httpResponseCode == 404) { + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build(); + } + // Usually the FIS server recovers from errors: retry one time before giving up. if (httpResponseCode >= 500 && httpResponseCode < 600) { retryCount++; @@ -259,7 +269,7 @@ private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { // Read the response from the createFirebaseInstallation API. private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); - InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); + TokenResult.Builder tokenResult = TokenResult.builder(); InstallationResponse.Builder builder = InstallationResponse.builder(); reader.beginObject(); while (reader.hasNext()) { @@ -275,15 +285,15 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws while (reader.hasNext()) { String key = reader.nextName(); if (key.equals("token")) { - installationTokenResult.setToken(reader.nextString()); + tokenResult.setToken(reader.nextString()); } else if (key.equals("expiresIn")) { - installationTokenResult.setTokenExpirationTimestamp( + tokenResult.setTokenExpirationTimestamp( parseTokenExpirationTimestamp(reader.nextString())); } else { reader.skipValue(); } } - builder.setAuthToken(installationTokenResult.build()); + builder.setAuthToken(tokenResult.build()); reader.endObject(); } else { reader.skipValue(); @@ -295,10 +305,9 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws } // Read the response from the generateAuthToken FirebaseInstallation API. - private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) - throws IOException { + private TokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) throws IOException { JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); - InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); + TokenResult.Builder builder = TokenResult.builder(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); @@ -312,7 +321,7 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection } reader.endObject(); - return builder.build(); + return builder.setResponseCode(TokenResult.ResponseCode.OK).build(); } // Read the error message from the response. diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index 8bea66ff2e4..0c0463b6f49 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -17,7 +17,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; -import com.google.firebase.installations.InstallationTokenResult; @AutoValue public abstract class InstallationResponse { @@ -39,7 +38,7 @@ public enum ResponseCode { public abstract String getRefreshToken(); @Nullable - public abstract InstallationTokenResult getAuthToken(); + public abstract TokenResult getAuthToken(); @Nullable public abstract ResponseCode getResponseCode(); @@ -65,7 +64,7 @@ public abstract static class Builder { public abstract Builder setRefreshToken(@NonNull String value); @NonNull - public abstract Builder setAuthToken(@NonNull InstallationTokenResult value); + public abstract Builder setAuthToken(@NonNull TokenResult value); @NonNull public abstract Builder setResponseCode(@NonNull ResponseCode value); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java new file mode 100644 index 00000000000..a120e418ff0 --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java @@ -0,0 +1,73 @@ +// 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.installations.remote; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.auto.value.AutoValue; + +/** This class represents a set of values describing a FIS Auth Token Result. */ +@AutoValue +public abstract class TokenResult { + + public enum ResponseCode { + // Returned on success + OK, + // Auth token cannot be generated for this FID in the request. Because it is not + // registered/found on the FIS server. Recreate a new fid to fetch a valid auth token. + FID_ERROR, + // Refresh token in this request in not accepted by the FIS server. Either it has been blocked + // or changed. Recreate a new fid to fetch a valid auth token. + REFRESH_TOKEN_ERROR, + } + + public boolean isSuccessful() { + return getResponseCode() == ResponseCode.OK; + } + + /** A new FIS Auth-Token, created for this Firebase Installation. */ + @Nullable + public abstract String getToken(); + /** The timestamp, before the auth-token expires for this Firebase Installation. */ + @NonNull + public abstract long getTokenExpirationTimestamp(); + + @Nullable + public abstract ResponseCode getResponseCode(); + + @NonNull + public abstract Builder toBuilder(); + + /** Returns a default Builder object to create an InstallationResponse object */ + @NonNull + public static TokenResult.Builder builder() { + return new AutoValue_TokenResult.Builder().setTokenExpirationTimestamp(0); + } + + @AutoValue.Builder + public abstract static class Builder { + @NonNull + public abstract Builder setToken(@NonNull String value); + + @NonNull + public abstract Builder setTokenExpirationTimestamp(long value); + + @NonNull + public abstract Builder setResponseCode(@NonNull ResponseCode value); + + @NonNull + public abstract TokenResult build(); + } +} From 3adccdacd6a7d6f1717f06517ec9ea8e459a3797 Mon Sep 17 00:00:00 2001 From: Ankita Date: Tue, 3 Dec 2019 08:32:30 -0800 Subject: [PATCH 64/74] Renaming getAuthToken to getToken. (#1026) --- .../FirebaseInstallationsApi.java | 4 +- firebase-installations/api.txt | 2 +- ...FirebaseInstallationsInstrumentedTest.java | 51 +++++++++---------- .../PersistedInstallationEntrySubject.java | 2 +- .../installations/FirebaseInstallations.java | 2 +- 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java index 7ecdb6a9961..fb08cb808dc 100644 --- a/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java +++ b/firebase-installations-interop/src/main/java/com/google/firebase/installations/FirebaseInstallationsApi.java @@ -39,7 +39,7 @@ public interface FirebaseInstallationsApi { int DO_NOT_FORCE_REFRESH = 0; /** * AuthToken is forcefully refreshed on calling the {@link - * FirebaseInstallationsApi#getAuthToken(int)}. + * FirebaseInstallationsApi#getToken(int)}. */ int FORCE_REFRESH = 1; @@ -50,7 +50,7 @@ public interface FirebaseInstallationsApi { Task getId(); /** Async function that returns a auth token(public key) of this Firebase app installation. */ - Task getAuthToken(@AuthTokenOption int authTokenOption); + Task getToken(@AuthTokenOption int authTokenOption); /** * Async function that deletes this Firebase app installation from Firebase backend. This call diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 06028ad7b5d..630322f0856 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -3,10 +3,10 @@ package com.google.firebase.installations { public class FirebaseInstallations { method @NonNull public Task delete(); - method @NonNull public Task getAuthToken(int); method @NonNull public Task getId(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull FirebaseApp); + method @NonNull public Task getToken(int); } public class FirebaseInstallationsException { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 26ae5acd6c8..1f8bb1de203 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -489,7 +489,7 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); @@ -510,8 +510,7 @@ public void testGetAuthToken_PersistedInstallationError_failure() throws Excepti // Expect exception try { - Tasks.await( - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); fail("Could not update local storage."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") @@ -540,8 +539,7 @@ public void testGetAuthToken_fidExists_successful() throws Exception { mockIidStore); InstallationTokenResult installationTokenResult = - Tasks.await( - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) @@ -551,7 +549,7 @@ public void testGetAuthToken_fidExists_successful() throws Exception { } @Test - public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { + public void testGetToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); @@ -559,8 +557,7 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = - Tasks.await( - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) @@ -570,8 +567,8 @@ public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Ex } @Test - public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { - // Update local storage with a unregistered installation entry to validate that getAuthToken + public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { + // Update local storage with a unregistered installation entry to validate that getToken // calls getId to ensure FID registration and returns a valid auth token. persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); @@ -579,8 +576,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); InstallationTokenResult installationTokenResult = - Tasks.await( - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) @@ -590,7 +586,7 @@ public void testGetAuthToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exc } @Test - public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exception { + public void testGetToken_fidError_persistedInstallationCleared() throws Exception { // Update local storage with an expired installation entry to ensure that generate auth token // is called. persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); @@ -606,8 +602,8 @@ public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exce // Expect exception try { - Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getAuthToken() failed due to Server Error."); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); + fail("getToken() failed due to Server Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -626,7 +622,7 @@ public void testGetAuthToken_fidError_persistedInstallationCleared() throws Exce } @Test - public void testGetAuthToken_serverError_failure() throws Exception { + public void testGetToken_serverError_failure() throws Exception { when(mockPersistedInstallation.readPersistedInstallationEntryValue()) .thenReturn(REGISTERED_INSTALLATION_ENTRY); when(backendClientReturnsError.generateAuthToken( @@ -645,8 +641,8 @@ public void testGetAuthToken_serverError_failure() throws Exception { // Expect exception try { - Tasks.await(firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getAuthToken() failed due to Server Error."); + Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); + fail("getToken() failed due to Server Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -659,8 +655,7 @@ public void testGetAuthToken_serverError_failure() throws Exception { } @Test - public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() - throws Exception { + public void testGetToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() throws Exception { // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. @@ -670,11 +665,11 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - // Call getAuthToken multiple times with DO_NOT_FORCE_REFRESH option + // Call getToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); Task task2 = - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); Tasks.await(Tasks.whenAllComplete(task1, task2)); @@ -689,10 +684,10 @@ public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce( } @Test - public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { + public void testGetToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not - // completed before the second task starts. Hence, we can test multiple calls to getAuthToken() + // completed before the second task starts. Hence, we can test multiple calls to getToken() // and verify one task waits for another task to complete. doAnswer( @@ -719,11 +714,11 @@ public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() th FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - // Call getAuthToken multiple times with FORCE_REFRESH option. + // Call getToken multiple times with FORCE_REFRESH option. Task task1 = - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH); + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); Task task2 = - firebaseInstallations.getAuthToken(FirebaseInstallationsApi.FORCE_REFRESH); + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); Tasks.await(Tasks.whenAllComplete(task1, task2)); // As we cannot ensure which task got executed first, verifying with both expected values diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java index 84c738ef42c..702b4684d2a 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java @@ -63,7 +63,7 @@ public void hasFid(String fid) { } public void hasAuthToken(String authToken) { - check("getAuthToken()").that(actual.getAuthToken()).isEqualTo(authToken); + check("getToken()").that(actual.getAuthToken()).isEqualTo(authToken); } public void hasRefreshToken(String refreshToken) { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index b0aff260fd1..9dc2ec666d1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -151,7 +151,7 @@ public Task getId() { */ @NonNull @Override - public Task getAuthToken(@AuthTokenOption int authTokenOption) { + public Task getToken(@AuthTokenOption int authTokenOption) { Task task = addGetAuthTokenListener(authTokenOption); executor.execute(this::doRegistration); return task; From fdbc752d5cc09776191409756c70db69047e036c Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Thu, 5 Dec 2019 13:27:55 -0500 Subject: [PATCH 65/74] Add heartbeat to fis sdk (#1037) * add tests * update api * installations --- firebase-installations/api.txt | 2 +- .../installations/FirebaseInstallations.java | 11 ++++++-- .../FirebaseInstallationsRegistrar.java | 11 +++++++- .../FirebaseInstallationServiceClient.java | 27 ++++++++++++++++++- .../InAppMessagingDisplay.java | 6 ++--- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 630322f0856..4acee522a18 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -79,7 +79,7 @@ package com.google.firebase.installations.local { package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { - ctor public FirebaseInstallationServiceClient(@NonNull Context); + ctor public FirebaseInstallationServiceClient(@NonNull Context, @Nullable UserAgentPublisher, @Nullable HeartBeatInfo); method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 9dc2ec666d1..09d5a67d67e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -16,6 +16,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; import com.google.android.gms.common.util.DefaultClock; @@ -24,6 +25,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.heartbeatinfo.HeartBeatInfo; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; @@ -32,6 +34,7 @@ import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.installations.remote.TokenResult; +import com.google.firebase.platforminfo.UserAgentPublisher; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -68,11 +71,15 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final List listeners = new ArrayList<>(); /** package private constructor. */ - FirebaseInstallations(FirebaseApp firebaseApp) { + FirebaseInstallations( + FirebaseApp firebaseApp, + @Nullable UserAgentPublisher publisher, + @Nullable HeartBeatInfo heartbeatInfo) { this( new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), firebaseApp, - new FirebaseInstallationServiceClient(firebaseApp.getApplicationContext()), + new FirebaseInstallationServiceClient( + firebaseApp.getApplicationContext(), publisher, heartbeatInfo), new PersistedInstallation(firebaseApp), new Utils(DefaultClock.getInstance()), new IidStore()); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java index e84168ab4fb..10731b9d11e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsRegistrar.java @@ -19,7 +19,9 @@ import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.heartbeatinfo.HeartBeatInfo; import com.google.firebase.platforminfo.LibraryVersionComponent; +import com.google.firebase.platforminfo.UserAgentPublisher; import java.util.Arrays; import java.util.List; @@ -32,7 +34,14 @@ public List> getComponents() { return Arrays.asList( Component.builder(FirebaseInstallationsApi.class) .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new FirebaseInstallations(c.get(FirebaseApp.class))) + .add(Dependency.required(HeartBeatInfo.class)) + .add(Dependency.required(UserAgentPublisher.class)) + .factory( + c -> + new FirebaseInstallations( + c.get(FirebaseApp.class), + c.get(UserAgentPublisher.class), + c.get(HeartBeatInfo.class))) .build(), LibraryVersionComponent.create("fire-installations", BuildConfig.VERSION_NAME)); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index bc5947bbe7c..a981635c02c 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -22,11 +22,15 @@ import android.util.JsonReader; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.util.AndroidUtilsLight; import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.FirebaseException; +import com.google.firebase.heartbeatinfo.HeartBeatInfo; +import com.google.firebase.heartbeatinfo.HeartBeatInfo.HeartBeat; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -55,6 +59,12 @@ public class FirebaseInstallationServiceClient { private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; private static final String GZIP_CONTENT_ENCODING = "gzip"; + /** Heartbeat tag for firebase installations. */ + private static final String FIREBASE_INSTALLATIONS_ID_HEARTBEAT_TAG = "fire-installations-id"; + + private static final String HEART_BEAT_HEADER = "x-firebase-client-log-type"; + private static final String USER_AGENT_HEADER = "x-firebase-client"; + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error: %s"; @@ -72,9 +82,16 @@ public class FirebaseInstallationServiceClient { static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp."; private final Context context; + private final UserAgentPublisher userAgentPublisher; + private final HeartBeatInfo heartbeatInfo; - public FirebaseInstallationServiceClient(@NonNull Context context) { + public FirebaseInstallationServiceClient( + @NonNull Context context, + @Nullable UserAgentPublisher publisher, + @Nullable HeartBeatInfo heartBeatInfo) { this.context = context; + this.userAgentPublisher = publisher; + this.heartbeatInfo = heartBeatInfo; } /** @@ -261,6 +278,14 @@ private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + if (heartbeatInfo != null && userAgentPublisher != null) { + HeartBeat heartbeat = heartbeatInfo.getHeartBeatCode(FIREBASE_INSTALLATIONS_ID_HEARTBEAT_TAG); + if (heartbeat != HeartBeat.NONE) { + httpsURLConnection.addRequestProperty(USER_AGENT_HEADER, userAgentPublisher.getUserAgent()); + httpsURLConnection.addRequestProperty( + HEART_BEAT_HEADER, Integer.toString(heartbeat.getCode())); + } + } httpsURLConnection.addRequestProperty( X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); return httpsURLConnection; diff --git a/tools/measurement/apksize/src/inappmessagingdisplay/java/com.google.apksize/InAppMessagingDisplay.java b/tools/measurement/apksize/src/inappmessagingdisplay/java/com.google.apksize/InAppMessagingDisplay.java index ec6c284dab4..59ac8525a50 100644 --- a/tools/measurement/apksize/src/inappmessagingdisplay/java/com.google.apksize/InAppMessagingDisplay.java +++ b/tools/measurement/apksize/src/inappmessagingdisplay/java/com.google.apksize/InAppMessagingDisplay.java @@ -16,16 +16,16 @@ import android.app.Activity; import android.content.Context; +import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayCallbacks; +import com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay; import com.google.firebase.inappmessaging.model.Action; import com.google.firebase.inappmessaging.model.CampaignMetadata; +import com.google.firebase.inappmessaging.model.InAppMessage; import com.google.firebase.inappmessaging.model.ModalMessage; import com.google.firebase.inappmessaging.model.Text; -import com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay; -import androidx.annotation.NonNull; -import com.google.firebase.inappmessaging.model.InAppMessage; public class InAppMessagingDisplay implements SampleCode { private static final String SAMPLE_TEXT = "My sample text"; From 56d72ee8af046fb94dcb5978ea4a907880acebd3 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Mon, 9 Dec 2019 17:25:34 -0800 Subject: [PATCH 66/74] Fixes for multi-process access (#1042) * Switch from using SharedPreferences to a flat file * Fixes for multi-process access - switch from shared prefs to a flat file - protect generate Fid for cross-process and cross-thread accesses - make doRegistration only read the prefs once at the beginning and clean up the flow - pass the forceRefresh flag into doRegistration rather than storing it in a global * revert the name of the FID prefs write call * fix formatting issues * Fix the cross process locking The check if a new FID was needed was not in the critical region. * some small changes from the review * fix the java format and update the api file --- firebase-installations/api.txt | 29 +- .../firebase/installations/FakeCalendar.java | 69 ++ ...FirebaseInstallationsInstrumentedTest.java | 792 +++++++++--------- .../FisAndroidTestConstants.java | 8 +- .../local/PersistedInstallationTest.java | 49 +- .../installations/FirebaseInstallations.java | 377 +++++---- .../FirebaseInstallationsException.java | 11 +- .../installations/GetAuthTokenListener.java | 10 +- .../firebase/installations/GetIdListener.java | 7 +- .../installations/RandomFidGenerator.java | 84 ++ .../firebase/installations/StateListener.java | 3 +- .../google/firebase/installations/Utils.java | 94 +-- .../local/PersistedInstallation.java | 178 ++-- .../local/PersistedInstallationEntry.java | 66 +- .../FirebaseInstallationServiceClient.java | 208 ++--- .../remote/InstallationResponse.java | 5 +- .../installations/remote/TokenResult.java | 8 +- 17 files changed, 1119 insertions(+), 879 deletions(-) create mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 4acee522a18..b4fd32edc8a 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -17,9 +17,12 @@ package com.google.firebase.installations { } public enum FirebaseInstallationsException.Status { - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status AUTHENTICATION_ERROR; - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status CLIENT_ERROR; - enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status SDK_INTERNAL_ERROR; + enum_constant public static final com.google.firebase.installations.FirebaseInstallationsException.Status BAD_CONFIG; + } + + public class RandomFidGenerator { + ctor public RandomFidGenerator(); + method @NonNull public String createRandomFid(); } } @@ -33,12 +36,13 @@ package com.google.firebase.installations.local { public class PersistedInstallation { ctor public PersistedInstallation(@NonNull FirebaseApp); - method @NonNull public boolean clear(); - method @NonNull public boolean insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); + method public void clearForTesting(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry readPersistedInstallationEntryValue(); } public enum PersistedInstallation.RegistrationStatus { + enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus ATTEMPT_MIGRATION; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus NOT_GENERATED; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTERED; enum_constant public static final com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus REGISTER_ERROR; @@ -59,7 +63,15 @@ package com.google.firebase.installations.local { method public boolean isNotGenerated(); method public boolean isRegistered(); method public boolean isUnregistered(); + method public boolean shouldAttemptMigration(); method @NonNull public abstract com.google.firebase.installations.local.PersistedInstallationEntry.Builder toBuilder(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withAuthToken(@NonNull String, long, long); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withClearedAuthToken(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withFisError(@NonNull String); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withNoGeneratedFid(); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withRegisteredFid(@NonNull String, @NonNull String, long, @Nullable String, long); + method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry withUnregisteredFid(@NonNull String); + field @NonNull public static com.google.firebase.installations.local.PersistedInstallationEntry INSTANCE; } public abstract static class PersistedInstallationEntry.Builder { @@ -107,8 +119,8 @@ package com.google.firebase.installations.remote { } public enum InstallationResponse.ResponseCode { + enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode BAD_CONFIG; enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode OK; - enum_constant public static final com.google.firebase.installations.remote.InstallationResponse.ResponseCode SERVER_ERROR; } public abstract class TokenResult { @@ -117,7 +129,6 @@ package com.google.firebase.installations.remote { method @Nullable public abstract com.google.firebase.installations.remote.TokenResult.ResponseCode getResponseCode(); method @Nullable public abstract String getToken(); method @NonNull public abstract long getTokenExpirationTimestamp(); - method public boolean isSuccessful(); method @NonNull public abstract com.google.firebase.installations.remote.TokenResult.Builder toBuilder(); } @@ -130,9 +141,9 @@ package com.google.firebase.installations.remote { } public enum TokenResult.ResponseCode { - enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode FID_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode AUTH_ERROR; + enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode BAD_CONFIG; enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode OK; - enum_constant public static final com.google.firebase.installations.remote.TokenResult.ResponseCode REFRESH_TOKEN_ERROR; } } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java new file mode 100644 index 00000000000..3190d5911e8 --- /dev/null +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java @@ -0,0 +1,69 @@ +// 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.installations; + +import java.util.Calendar; + +public class FakeCalendar extends Calendar { + private long timeInMillis; + + public FakeCalendar(long initialTimeInMillis) { + timeInMillis = initialTimeInMillis; + } + + public long getTimeInMillis() { + return timeInMillis; + } + + public void setTimeInMillis(long timeInMillis) { + this.timeInMillis = timeInMillis; + } + + public void advanceTimeBySeconds(long deltaSeconds) { + timeInMillis += (deltaSeconds * 1000L); + } + + @Override + protected void computeTime() {} + + @Override + protected void computeFields() {} + + @Override + public void add(int i, int i1) {} + + @Override + public void roll(int i, boolean b) {} + + @Override + public int getMinimum(int i) { + return 0; + } + + @Override + public int getMaximum(int i) { + return 0; + } + + @Override + public int getGreatestMinimum(int i) { + return 0; + } + + @Override + public int getLeastMaximum(int i) { + return 0; + } +} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 1f8bb1de203..4fec8c4cce5 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -15,16 +15,12 @@ package com.google.firebase.installations; import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.firebase.installations.FisAndroidTestConstants.DEFAULT_PERSISTED_INSTALLATION_ENTRY; -import static com.google.firebase.installations.FisAndroidTestConstants.INVALID_TEST_FID; -import static com.google.firebase.installations.FisAndroidTestConstants.SERVER_ERROR_INSTALLATION_RESPONSE; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_3; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; @@ -33,18 +29,19 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP_2; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import androidx.test.core.app.ApplicationProvider; @@ -54,12 +51,16 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.installations.remote.TokenResult; +import java.io.IOException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -86,13 +87,9 @@ public class FirebaseInstallationsInstrumentedTest { private FirebaseApp firebaseApp; private ExecutorService executor; private PersistedInstallation persistedInstallation; - @Mock private FirebaseInstallationServiceClient backendClientReturnsOk; - @Mock private FirebaseInstallationServiceClient backendClientReturnsError; - @Mock private PersistedInstallation persistedInstallationReturnsError; - @Mock private Utils mockUtils; - @Mock private PersistedInstallation mockPersistedInstallation; - @Mock private FirebaseInstallationServiceClient mockClient; + @Mock private FirebaseInstallationServiceClient mockBackend; @Mock private IidStore mockIidStore; + @Mock private RandomFidGenerator mockFidGenerator; private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder() @@ -104,61 +101,16 @@ public class FirebaseInstallationsInstrumentedTest { .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) .build(); - private static final PersistedInstallationEntry REGISTERED_IID_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_INSTANCE_ID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private static final PersistedInstallationEntry EXPIRED_AUTH_TOKEN_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP_2) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private static final PersistedInstallationEntry UNREGISTERED_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken("") - .setRefreshToken("") - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .build(); - - private static final PersistedInstallationEntry INVALID_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(INVALID_TEST_FID) - .setAuthToken("") - .setRefreshToken("") - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .build(); - - private static final PersistedInstallationEntry UPDATED_AUTH_TOKEN_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN_2) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); + private FirebaseInstallations firebaseInstallations; + private Utils utils; + private FakeCalendar fakeCalendar; @Before - public void setUp() throws FirebaseException { + public void setUp() throws FirebaseException, IOException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + fakeCalendar = new FakeCalendar(5000000L); firebaseApp = FirebaseApp.initializeApp( ApplicationProvider.getApplicationContext(), @@ -168,69 +120,188 @@ public void setUp() throws FirebaseException { .setApiKey(TEST_API_KEY) .build()); persistedInstallation = new PersistedInstallation(firebaseApp); + persistedInstallation.clearForTesting(); - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - // Mocks successful auth token generation - when(backendClientReturnsOk.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); + utils = new Utils(fakeCalendar); + firebaseInstallations = + new FirebaseInstallations( + executor, + firebaseApp, + mockBackend, + persistedInstallation, + utils, + mockIidStore, + mockFidGenerator); - when(persistedInstallationReturnsError.insertOrUpdatePersistedInstallationEntry(any())) - .thenReturn(false); - when(persistedInstallationReturnsError.readPersistedInstallationEntryValue()) - .thenReturn(DEFAULT_PERSISTED_INSTALLATION_ENTRY); + when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); + } - when(backendClientReturnsError.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenThrow(new FirebaseException("SDK Error")); + @After + public void cleanUp() { + persistedInstallation.clearForTesting(); + try { + executor.awaitTermination(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { - when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); - when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2); + } + } - // Mocks success on FIS deletion - doNothing() - .when(backendClientReturnsOk) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - // Mocks server error on FIS deletion - doThrow(new FirebaseException("Server Error")) - .when(backendClientReturnsError) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + /** + * Check the id generation process when there is no network. There are three cases: + * + *

    + *
  • no iid -> generate a new fid + *
  • iid present -> make that iid into a fid + *
  • fid generated -> return that fid + *
+ */ + @Test + public void testGetId_noNetwork_noIid() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(null); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } - @After - public void cleanUp() throws Exception { - persistedInstallation.clear(); + @Test + public void testGetId_noNetwork_iidPresent() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } - private FirebaseInstallations getFirebaseInstallations() { - return new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallation, - mockUtils, - mockIidStore); + @Test + public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); + + // Do the actual getId() call under test. Confirm that it returns the already generated FID. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo("generatedFid"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(updatedInstallationEntry).hasFid("generatedFid"); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } + /** + * Checks that if we have a registered fid then the fid is returned and no backend calls are made. + */ @Test - public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_IID_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); // No exception, means success. assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = + .isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verifyZeroInteractions(mockBackend); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); + } + + /** + * Checks that if we have an unregistered fid that the fid gets registered with the backend and no + * other calls are made. + */ + @Test + public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), matches(TEST_FID_1), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // No exception, means success. + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second // while we mock fid registration. We dont send an actual request to FIS in tests. executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verify(mockBackend) + .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); + verify(mockBackend, never()) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // check that the fid is still the expected one and is registered PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); @@ -240,16 +311,14 @@ public void testGetId_PersistedInstallationOk_BackendOk() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - // No exception, means success. + // Do the actual getId() call under test. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); + .isEqualTo(TEST_INSTANCE_ID_1); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); @@ -257,6 +326,8 @@ public void testGetId_migrateIid_successful() throws Exception { // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // The storage should still have the same ID and the status should indicate that the + // fid si registered. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); @@ -265,11 +336,9 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) + when(mockIidStore.readIid()).thenReturn(null); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) .thenReturn(TEST_INSTALLATION_RESPONSE); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getId multiple times Task task1 = firebaseInstallations.getId(); @@ -284,7 +353,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { assertWithMessage("Persisted Fid of Task2 doesn't match.") .that(task2.getResult()) .isEqualTo(TEST_FID_1); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); @@ -292,203 +361,172 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + /** + * Checks that if the server rejects a FID during registration the SDK will use the fid in the + * response as the new fid. + */ @Test - public void testGetId_invalidFid_storesValidFidFromResponse() throws Exception { + public void testGetId_unregistered_replacesFidWithResponse() throws Exception { // Update local storage with installation entry that has invalid fid. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(INVALID_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); - // No exception, means success. + // The first call will return the existing FID, "tobereplaced" assertWithMessage("getId Task failed.") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(INVALID_TEST_FID); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - // After FID registration is complete, installation entry is updated with valid fid. - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - @Test - public void testGetId_PersistedInstallationOk_BackendError() throws Exception { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - persistedInstallation, - mockUtils, - mockIidStore); - - Tasks.await(firebaseInstallations.getId()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + .isEqualTo("tobereplaced"); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + // The next call should return the FID that was returned by the server + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); } + /** + * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error + * state. + */ @Test public void testGetId_ServerError_UnregisteredFID() throws Exception { - // Mocking server error on FIS createFirebaseInstallation, returns empty InstallationResponse - when(backendClientReturnsOk.createFirebaseInstallation( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(SERVER_ERROR_INSTALLATION_RESPONSE); + // start with an unregistered fid + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - - Tasks.await(firebaseInstallations.getId()); + // have the server return a server error for the registration + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.") + .that(Tasks.await(firebaseInstallations.getId())) + .isEqualTo(TEST_FID_1); - // Waiting for Task that registers FID on the FIS Servers + // Waiting for Task that registers FID on the FIS Servers. executor.awaitTermination(500, TimeUnit.MILLISECONDS); + // We expect that the server error will cause the FID to be put into the error state. + // There is nothing more we can do. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_PersistedInstallationError_BackendOk() throws InterruptedException { - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallationReturnsError, - mockUtils, - mockIidStore); - - // Expect exception - try { - Tasks.await(firebaseInstallations.getId()); - fail("Could not update local storage."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); - } + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); } + /** + * A registration that fails with an IOException will not cause the FID to be put into the error + * state. + */ @Test public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { - // Mocking unchecked exception on FIS createFirebaseInstallation - when(mockClient.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenAnswer( - invocation -> { - throw new InterruptedException(); - }); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); + // set initial state to having an unregistered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - Tasks.await(firebaseInstallations.getId()); + // Mocking unchecked exception on FIS createFirebaseInstallation + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + String fid = Tasks.await(firebaseInstallations.getId()); + assertEquals("fid doesn't match expected", TEST_FID_1, fid); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - // Validate that registration status is REGISTER_ERROR + // We expect that the IOException will cause the request to fail, but it will not + // cause the FID to be put into the error state because we expect this to eventually succeed. PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); } @Test public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { - // Update local storage with installation entry that has auth token expired. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + // Mocking unchecked exception on FIS generateAuthToken - when(mockClient.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenAnswer( - invocation -> { - throw new InterruptedException(); - }); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, firebaseApp, mockClient, persistedInstallation, mockUtils, mockIidStore); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + .isEqualTo(TEST_FID_1); // Waiting for Task that generates auth token with the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - // Validate that registration status is REGISTER_ERROR + // Validate that registration status is still REGISTER PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); } + /** + * The FID is successfully registered but the token is expired. A getId will cause the token to be + * refreshed in the background. + */ @Test public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { - // Update local storage with installation entry that has auth token expired. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + // Get the ID, which should cause the SDK to realize that the auth token is expired and + // kick off a refresh of the token. assertWithMessage("getId Task failed") .that(Tasks.await(firebaseInstallations.getId())) - .isNotEmpty(); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); + .isEqualTo(TEST_FID_1); // Waiting for Task that registers FID on the FIS Servers executor.awaitTermination(500, TimeUnit.MILLISECONDS); - // Validate that Persisted FID has a refreshed auth token now - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, never()) + // Check that the token has been refreshed + assertWithMessage("auth token is not what is expected after the refresh") + .that( + Tasks.await( + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)) + .getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + + verify(mockBackend, never()) .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); - + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); PersistedInstallationEntry entryValue = @@ -496,47 +534,15 @@ public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); } - @Test - public void testGetAuthToken_PersistedInstallationError_failure() throws Exception { - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - persistedInstallationReturnsError, - mockUtils, - mockIidStore); - - // Expect exception - try { - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - fail("Could not update local storage."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.CLIENT_ERROR); - } - } - @Test public void testGetAuthToken_fidExists_successful() throws Exception { - when(mockPersistedInstallation.readPersistedInstallationEntryValue()) - .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsOk, - mockPersistedInstallation, - mockUtils, - mockIidStore); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); InstallationTokenResult installationTokenResult = Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -544,17 +550,21 @@ public void testGetAuthToken_fidExists_successful() throws Exception { assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); - verify(backendClientReturnsOk, never()) + verify(mockBackend, never()) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test - public void testGetToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); + public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { + // start with a registered FID and valid auth token + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // have the server respond with a new token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); InstallationTokenResult installationTokenResult = Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -562,18 +572,16 @@ public void testGetToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Except assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { // Update local storage with a unregistered installation entry to validate that getToken // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); InstallationTokenResult installationTokenResult = Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); @@ -581,68 +589,63 @@ public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Excepti assertWithMessage("Persisted Auth Token doesn't match") .that(installationTokenResult.getToken()) .isEqualTo(TEST_AUTH_TOKEN); - verify(backendClientReturnsOk, times(1)) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); } @Test - public void testGetToken_fidError_persistedInstallationCleared() throws Exception { - // Update local storage with an expired installation entry to ensure that generate auth token - // is called. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); + public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + // Mocks error during auth token generation - when(backendClientReturnsOk.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build()); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)) - .thenReturn(/* isExpired*/ true, /*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); // Expect exception try { Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getToken() failed due to Server Error."); + fail("the getAuthToken() call should have failed due to Auth Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.AUTHENTICATION_ERROR); + .isInstanceOf(IOException.class); } - verify(backendClientReturnsOk, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); } + // /** + // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call + // * fails. + // */ @Test - public void testGetToken_serverError_failure() throws Exception { - when(mockPersistedInstallation.readPersistedInstallationEntryValue()) - .thenReturn(REGISTERED_INSTALLATION_ENTRY); - when(backendClientReturnsError.generateAuthToken( - anyString(), anyString(), anyString(), anyString())) - .thenThrow(new FirebaseException("Server Error")); - when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - mockPersistedInstallation, - mockUtils, - mockIidStore); + public void testGetAuthToken_serverError_failure() throws Exception { + // start the test with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // have the backend fail when generateAuthToken is invoked. + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); - // Expect exception + // Make the forced getAuthToken call, which should fail. try { Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("getToken() failed due to Server Error."); + fail( + "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " + + "returned by the network call."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -650,20 +653,28 @@ public void testGetToken_serverError_failure() throws Exception { .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + .isEqualTo(Status.BAD_CONFIG); } } @Test - public void testGetToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() throws Exception { - // Update local storage with a EXPIRED_AUTH_TOKEN_ENTRY to validate the flow of multiple tasks - // triggered simultaneously. Task2 waits for Task1 to complete. On task1 completion, task2 reads - // the UPDATED_AUTH_TOKEN_FID_ENTRY generated by Task1. - persistedInstallation.insertOrUpdatePersistedInstallationEntry(EXPIRED_AUTH_TOKEN_ENTRY); - when(mockUtils.isAuthTokenExpired(EXPIRED_AUTH_TOKEN_ENTRY)).thenReturn(/*isExpired*/ true); - when(mockUtils.isAuthTokenExpired(UPDATED_AUTH_TOKEN_ENTRY)).thenReturn(/*isValid*/ false); + public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() + throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + // expire the authtoken by advancing the clock + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); // Call getToken multiple times with DO_NOT_FORCE_REFRESH option Task task1 = @@ -679,13 +690,21 @@ public void testGetToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() th assertWithMessage("Persisted Auth Token doesn't match") .that(task2.getResult().getToken()) .isEqualTo(TEST_AUTH_TOKEN_2); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test - public void testGetToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not // completed before the second task starts. Hence, we can test multiple calls to getToken() // and verify one task waits for another task to complete. @@ -708,11 +727,8 @@ public void testGetToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setResponseCode(TokenResult.ResponseCode.OK) .build())) - .when(backendClientReturnsOk) + .when(mockBackend) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - when(mockUtils.isAuthTokenExpired(any())).thenReturn(/*isValid*/ false); - - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); // Call getToken multiple times with FORCE_REFRESH option. Task task1 = @@ -728,7 +744,7 @@ public void testGetToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws assertWithMessage("Persisted Auth Token doesn't match") .that(task2.getResult().getToken()) .isEqualTo(TEST_AUTH_TOKEN_3); - verify(backendClientReturnsOk, times(1)) + verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); PersistedInstallationEntry updatedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); @@ -739,62 +755,60 @@ public void testGetToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws public void testDelete_registeredFID_successful() throws Exception { // Update local storage with a registered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, times(1)) + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, times(1)) .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testDelete_unregisteredFID_successful() throws Exception { // Update local storage with a unregistered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(UNREGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, never()) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @Test public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - FirebaseInstallations firebaseInstallations = getFirebaseInstallations(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); Tasks.await(firebaseInstallations.delete()); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(DEFAULT_PERSISTED_INSTALLATION_ENTRY); - verify(backendClientReturnsOk, never()) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @Test - public void testDelete_serverError_failure() throws Exception { + public void testDelete_serverError_badConfig() throws Exception { // Update local storage with a registered installation entry persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - FirebaseInstallations firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - backendClientReturnsError, - persistedInstallation, - mockUtils, - mockIidStore); + + doThrow(new FirebaseException("Server Error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); // Expect exception try { Tasks.await(firebaseInstallations.delete()); - fail("delete() failed due to Server Error."); + fail("firebaseInstallations.delete() failed due to Server Error."); } catch (ExecutionException expected) { assertWithMessage("Exception class doesn't match") .that(expected) @@ -802,7 +816,31 @@ public void testDelete_serverError_failure() throws Exception { .isInstanceOf(FirebaseInstallationsException.class); assertWithMessage("Exception status doesn't match") .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + .isEqualTo(Status.BAD_CONFIG); + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); + } + } + + @Test + public void testDelete_networkError() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); + + doThrow(new IOException()) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + Tasks.await(firebaseInstallations.delete()); + fail("firebaseInstallations.delete() failed due to a Network Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); PersistedInstallationEntry entryValue = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index eb4a0f47f85..769ea8ec5a9 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -21,8 +21,6 @@ public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; - // Invalid FID. - public static final String INVALID_TEST_FID = "invalid"; public static final String TEST_PROJECT_ID = "777777777777"; @@ -38,8 +36,7 @@ public final class FisAndroidTestConstants { public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; public static final String TEST_APP_ID_2 = "1:987654321:android:abcdef"; - public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 1000L; - public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP_2 = 2000L; + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 4000L; public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; @@ -80,7 +77,4 @@ public final class FisAndroidTestConstants { .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) .setResponseCode(TokenResult.ResponseCode.OK) .build(); - - public static final InstallationResponse SERVER_ERROR_INSTALLATION_RESPONSE = - InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java index a27e1885924..495dcd38991 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationTest.java @@ -24,7 +24,6 @@ import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; -import static org.junit.Assert.assertTrue; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; @@ -62,9 +61,9 @@ public void setUp() { } @After - public void cleanUp() throws Exception { - persistedInstallation0.clear(); - persistedInstallation1.clear(); + public void cleanUp() { + persistedInstallation0.clearForTesting(); + persistedInstallation1.clearForTesting(); } @Test @@ -77,17 +76,16 @@ public void testReadPersistedInstallationEntry_Null() { @Test public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exception { - // Insert Persisted Installation Entry with Unregistered status in Shared Prefs - assertTrue( - persistedInstallation0.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build())); + // Write the Persisted Installation Entry with Unregistered status to storage. + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.UNREGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_1) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()); PersistedInstallationEntry entryValue = persistedInstallation0.readPersistedInstallationEntryValue(); @@ -99,17 +97,16 @@ public void testUpdateAndReadPersistedInstallationEntry_successful() throws Exce assertThat(entryValue).hasTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP); assertThat(entryValue).hasCreationTimestamp(TEST_CREATION_TIMESTAMP_1); - // Update Persisted Fid Entry with Registered status in Shared Prefs - assertTrue( - persistedInstallation0.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build())); + // Write the Persisted Fid Entry with Registered status to storage. + persistedInstallation0.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(TEST_FID_1) + .setAuthToken(TEST_AUTH_TOKEN) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) + .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) + .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()); entryValue = persistedInstallation0.readPersistedInstallationEntryValue(); // Validate update was successful diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 09d5a67d67e..3dc316a7b8e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -19,23 +19,27 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; import com.google.firebase.heartbeatinfo.HeartBeatInfo; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.local.IidStore; import com.google.firebase.installations.local.PersistedInstallation; -import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.installations.remote.TokenResult; import com.google.firebase.platforminfo.UserAgentPublisher; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.util.ArrayList; +import java.util.Calendar; import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; @@ -55,21 +59,23 @@ * */ public class FirebaseInstallations implements FirebaseInstallationsApi { - private final FirebaseApp firebaseApp; private final FirebaseInstallationServiceClient serviceClient; private final PersistedInstallation persistedInstallation; private final ExecutorService executor; private final Utils utils; private final IidStore iidStore; + private final RandomFidGenerator fidGenerator; private final Object lock = new Object(); - @GuardedBy("lock") - private boolean shouldRefreshAuthToken; - @GuardedBy("lock") private final List listeners = new ArrayList<>(); + /* used for thread-level synchronization of generating and persisting fids */ + private final Object lockGenerateFid = new Object(); + /* file used for process-level syncronization of generating and persisting fids */ + private static final String LOCKFILE_NAME_GENERATE_FID = "generatefid.lock"; + /** package private constructor. */ FirebaseInstallations( FirebaseApp firebaseApp, @@ -81,8 +87,9 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { new FirebaseInstallationServiceClient( firebaseApp.getApplicationContext(), publisher, heartbeatInfo), new PersistedInstallation(firebaseApp), - new Utils(DefaultClock.getInstance()), - new IidStore()); + new Utils(Calendar.getInstance()), + new IidStore(), + new RandomFidGenerator()); } FirebaseInstallations( @@ -91,13 +98,15 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { FirebaseInstallationServiceClient serviceClient, PersistedInstallation persistedInstallation, Utils utils, - IidStore iidStore) { + IidStore iidStore, + RandomFidGenerator fidGenerator) { this.firebaseApp = firebaseApp; this.serviceClient = serviceClient; this.executor = executor; this.persistedInstallation = persistedInstallation; this.utils = utils; this.iidStore = iidStore; + this.fidGenerator = fidGenerator; } /** @@ -143,7 +152,7 @@ String getName() { @Override public Task getId() { Task task = addGetIdListener(); - executor.execute(this::doRegistration); + executor.execute(this::doGetId); return task; } @@ -159,8 +168,12 @@ public Task getId() { @NonNull @Override public Task getToken(@AuthTokenOption int authTokenOption) { - Task task = addGetAuthTokenListener(authTokenOption); - executor.execute(this::doRegistration); + Task task = addGetAuthTokenListener(); + if (authTokenOption == FORCE_REFRESH) { + executor.execute(this::doGetAuthTokenForceRefresh); + } else { + executor.execute(this::doGetAuthTokenWithoutForceRefresh); + } return task; } @@ -184,15 +197,11 @@ private Task addGetIdListener() { return taskCompletionSource.getTask(); } - private Task addGetAuthTokenListener( - @AuthTokenOption int authTokenOption) { + private Task addGetAuthTokenListener() { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); StateListener l = new GetAuthTokenListener(utils, taskCompletionSource); synchronized (lock) { - if (authTokenOption == FORCE_REFRESH) { - shouldRefreshAuthToken = true; - } listeners.add(l); } return taskCompletionSource.getTask(); @@ -203,8 +212,7 @@ private void triggerOnStateReached(PersistedInstallationEntry persistedInstallat Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = - l.onStateReached(persistedInstallationEntry, shouldRefreshAuthToken); + boolean doneListening = l.onStateReached(persistedInstallationEntry); if (doneListening) { it.remove(); } @@ -212,13 +220,12 @@ private void triggerOnStateReached(PersistedInstallationEntry persistedInstallat } } - private void triggerOnException( - PersistedInstallationEntry persistedInstallationEntry, Exception exception) { + private void triggerOnException(PersistedInstallationEntry prefs, Exception exception) { synchronized (lock) { Iterator it = listeners.iterator(); while (it.hasNext()) { StateListener l = it.next(); - boolean doneListening = l.onException(persistedInstallationEntry, exception); + boolean doneListening = l.onException(prefs, exception); if (doneListening) { it.remove(); } @@ -226,175 +233,199 @@ private void triggerOnException( } } - private final void doRegistration() { - try { - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); + private final void doGetId() { + doRegistrationInternal(false); + } - // New FID needs to be created - if (persistedInstallationEntry.isNotGenerated()) { + private final void doGetAuthTokenWithoutForceRefresh() { + doRegistrationInternal(false); + } - // For a default firebase installation read the existing iid. For other custom firebase - // installations create a new fid - String fid = readExistingIidOrCreateFid(); - persistFid(fid); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - } + private final void doGetAuthTokenForceRefresh() { + doRegistrationInternal(true); + } - if (persistedInstallationEntry.isErrored()) { - throw new FirebaseInstallationsException( - persistedInstallationEntry.getFisError(), - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - } + /** + * Logic for handling get id and the two forms of get auth token. This handles all the work, + * including creating a new FID if one hasn't been generated yet and making the network calls to + * create an installation and to retrieve a new auth token. Also contains the error handling for + * when the server says that credentials are bad and that a new Fid needs to be generated. + * + * @param forceRefresh true if this is for a getAuthToken call and if the caller wants to fetch a + * new auth token from the server even if an unexpired auth token exists on the client. + */ + private final void doRegistrationInternal(boolean forceRefresh) { + PersistedInstallationEntry prefs = getPrefsWithGeneratedIdMultiProcessSafe(); - triggerOnStateReached(persistedInstallationEntry); + // Since the caller wants to force an authtoken refresh remove the authtoken from the + // prefs we are working with, so the following steps know a new token is required. + if (forceRefresh) { + prefs = prefs.withClearedAuthToken(); + } - // FID needs to be registered - if (persistedInstallationEntry.isUnregistered()) { - registerAndSaveFid(persistedInstallationEntry); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - // Newly registered Fid will have valid auth token. No refresh required. - synchronized (lock) { - shouldRefreshAuthToken = false; - } + triggerOnStateReached(prefs); + + // There are two possible cleanup steps to perform at this stage: the FID may need to + // be registered with the server or the FID is registered but we need a fresh authtoken. + // Registering will also result in a fresh authtoken. Do the appropriate step here. + try { + if (prefs.isErrored() || prefs.isUnregistered()) { + prefs = registerFidWithServer(prefs); + } else if (forceRefresh || utils.isAuthTokenExpired(prefs)) { + prefs = fetchAuthTokenFromServer(prefs); + } else { + // nothing more to do, get out now + return; } + } catch (IOException e) { + triggerOnException(prefs, e); + return; + } - // Don't notify the listeners at this point; we might as well make ure the auth token is up - // to date before letting them know. + // Store the prefs to persist the result of the previous step. + persistedInstallation.insertOrUpdatePersistedInstallationEntry(prefs); + + // Let the caller know about the result. + if (prefs.isErrored()) { + triggerOnException(prefs, new FirebaseInstallationsException(Status.BAD_CONFIG)); + } else if (prefs.isNotGenerated()) { + // If there is no fid it means the call failed with an auth error. Simulate an + // IOException so that the caller knows to try again. + triggerOnException(prefs, new IOException("cleared fid due to auth error")); + } else { + triggerOnStateReached(prefs); + } + } - boolean needRefresh = utils.isAuthTokenExpired(persistedInstallationEntry); - if (!needRefresh) { - synchronized (lock) { - needRefresh = shouldRefreshAuthToken; + /** + * Loads the prefs, generating a new ID if necessary. This operation is made cross-process and + * cross-thread safe by wrapping all the processing first in a java synchronization block and + * wrapping that in a cross-process lock created using FileLocks. + * + *

If a FID does not yet exist it generate a new FID, either from an existing IID or generated + * randomly. If an IID exists and this is the first time a FID has been generated for this + * installation, the IID will be used as the FID. If the FID is ever cleared then the next time a + * FID is generated the IID is ignored and a FID is generated randomly. + * + * @return a new version of the prefs that includes the new FID. These prefs will have already + * been persisted. + */ + private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { + FileLock fileLock = getCrossProcessLock(); + try { + synchronized (lockGenerateFid) { + PersistedInstallationEntry prefs = + persistedInstallation.readPersistedInstallationEntryValue(); + // Check if a new FID needs to be created + if (prefs.isNotGenerated()) { + // For a default firebase installation read the existing iid. For other custom firebase + // installations create a new fid + + // Only one single thread from one single process can execute this block + // at any given time. + String fid = readExistingIidOrCreateFid(prefs); + prefs = + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + prefs.withUnregisteredFid(fid)); } + return prefs; } - TokenResult tokenResult = null; - // Refresh Auth token if needed - if (needRefresh) { - tokenResult = fetchAuthTokenFromServer(persistedInstallationEntry); - persistedInstallationEntry = persistedInstallation.readPersistedInstallationEntryValue(); - synchronized (lock) { - shouldRefreshAuthToken = false; - } - } + } finally { + releaseCrossProcessLock(fileLock); + } + } - // If tokenResult is not null and is not successful, it was cleared due to authentication - // error during auth token generation. - if (tokenResult != null && !tokenResult.isSuccessful()) { - triggerOnException( - persistedInstallationEntry, - new FirebaseInstallationsException( - "Failed to generate auth token for this Firebase Installation. Call getId() " - + "to recreate a new Fid and a valid auth token.", - FirebaseInstallationsException.Status.AUTHENTICATION_ERROR)); - return; - } + /** Use file locking to acquire a lock that will also block other processes. */ + private FileLock getCrossProcessLock() { + try { + File file = + new File(firebaseApp.getApplicationContext().getFilesDir(), LOCKFILE_NAME_GENERATE_FID); + FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); + // Use the file channel to create a lock on the file. + // This method blocks until it can retrieve the lock. + return channel.lock(); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); + } + } - triggerOnStateReached(persistedInstallationEntry); - } catch (Exception e) { - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - PersistedInstallationEntry errorInstallationEntry = - persistedInstallationEntry - .toBuilder() - .setFisError(e.getMessage()) - .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) - .build(); - persistedInstallation.insertOrUpdatePersistedInstallationEntry(errorInstallationEntry); - triggerOnException(errorInstallationEntry, e); + /** Release a previously acquired lock. */ + private void releaseCrossProcessLock(FileLock fileLock) { + try { + fileLock.release(); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); } } - private String readExistingIidOrCreateFid() { + private String readExistingIidOrCreateFid(PersistedInstallationEntry prefs) { // Check if this firebase app is the default (first initialized) instance - if (!firebaseApp.equals(FirebaseApp.getInstance())) { - return utils.createRandomFid(); + if (!firebaseApp.equals(FirebaseApp.getInstance()) || !prefs.shouldAttemptMigration()) { + return fidGenerator.createRandomFid(); } // For a default firebase installation, read the existing iid from shared prefs String fid = iidStore.readIid(); if (fid == null) { - fid = utils.createRandomFid(); + fid = fidGenerator.createRandomFid(); } return fid; } - private void persistFid(String fid) throws FirebaseInstallationsException { - boolean firstUpdateCacheResult = - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.UNREGISTERED) - .build()); - - if (!firstUpdateCacheResult) { - throw new FirebaseInstallationsException( - "Failed to update client side cache.", - FirebaseInstallationsException.Status.CLIENT_ERROR); + /** Registers the created Fid with FIS servers and update the persisted state. */ + private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEntry prefs) + throws IOException { + InstallationResponse response = + serviceClient.createFirebaseInstallation( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ prefs.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*appId= */ getApplicationId()); + + switch (response.getResponseCode()) { + case OK: + return prefs.withRegisteredFid( + response.getFid(), + response.getRefreshToken(), + utils.currentTimeInSecs(), + response.getAuthToken().getToken(), + response.getAuthToken().getTokenExpirationTimestamp()); + case BAD_CONFIG: + return prefs.withFisError("BAD CONFIG"); + default: + throw new IOException(); } } - /** Registers the created Fid with FIS servers and update the shared prefs. */ - private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallationEntry) - throws FirebaseInstallationsException { - try { - long creationTime = utils.currentTimeInSecs(); - - InstallationResponse installationResponse = - serviceClient.createFirebaseInstallation( - /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), - /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*appId= */ getApplicationId()); - if (installationResponse.getResponseCode() == ResponseCode.OK) { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(installationResponse.getFid()) - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(installationResponse.getAuthToken().getToken()) - .setRefreshToken(installationResponse.getRefreshToken()) - .setExpiresInSecs(installationResponse.getAuthToken().getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); - } - - } catch (FirebaseException exception) { - throw new FirebaseInstallationsException( - exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); - } - return null; - } - - /** Calls the FIS servers to generate an auth token for this Firebase installation. */ - private TokenResult fetchAuthTokenFromServer( - PersistedInstallationEntry persistedInstallationEntry) throws FirebaseInstallationsException { - try { - long creationTime = utils.currentTimeInSecs(); - TokenResult tokenResult = - serviceClient.generateAuthToken( - /*apiKey= */ firebaseApp.getOptions().getApiKey(), - /*fid= */ persistedInstallationEntry.getFirebaseInstallationId(), - /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*refreshToken= */ persistedInstallationEntry.getRefreshToken()); - - if (tokenResult.isSuccessful()) { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - persistedInstallationEntry - .toBuilder() - .setRegistrationStatus(RegistrationStatus.REGISTERED) - .setAuthToken(tokenResult.getToken()) - .setExpiresInSecs(tokenResult.getTokenExpirationTimestamp()) - .setTokenCreationEpochInSecs(creationTime) - .build()); - } else { - persistedInstallation.clear(); - } - return tokenResult; - - } catch (FirebaseException exception) { - throw new FirebaseInstallationsException( - "Failed to generate auth token for a Firebase Installation.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + /** + * Calls the FIS servers to generate an auth token for this Firebase installation. Returns a + * PersistedInstallationEntry with the new authtoken. The authtoken in the returned + * PersistedInstallationEntry will be "expired" if the server refuses to generate an auth token + * for the fid. + */ + private PersistedInstallationEntry fetchAuthTokenFromServer( + @NonNull PersistedInstallationEntry prefs) throws IOException { + TokenResult tokenResult = + serviceClient.generateAuthToken( + /*apiKey= */ firebaseApp.getOptions().getApiKey(), + /*fid= */ prefs.getFirebaseInstallationId(), + /*projectID= */ firebaseApp.getOptions().getProjectId(), + /*refreshToken= */ prefs.getRefreshToken()); + + switch (tokenResult.getResponseCode()) { + case OK: + return prefs.withAuthToken( + tokenResult.getToken(), + tokenResult.getTokenExpirationTimestamp(), + utils.currentTimeInSecs()); + case BAD_CONFIG: + return prefs.withFisError("BAD CONFIG"); + case AUTH_ERROR: + // The the server refused to generate a new auth token due to bad credentials, clear the + // FID to force the generation of a new one. + return prefs.withNoGeneratedFid(); + default: + throw new IOException(); } } @@ -402,28 +433,24 @@ private TokenResult fetchAuthTokenFromServer( * Deletes the firebase installation id of the {@link FirebaseApp} from FIS servers and local * storage. */ - private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException { - - PersistedInstallationEntry persistedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - - if (persistedInstallationEntry.isRegistered()) { + private Void deleteFirebaseInstallationId() throws FirebaseInstallationsException, IOException { + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + if (entry.isRegistered()) { // Call the FIS servers to delete this Firebase Installation Id. try { serviceClient.deleteFirebaseInstallation( firebaseApp.getOptions().getApiKey(), - persistedInstallationEntry.getFirebaseInstallationId(), + entry.getFirebaseInstallationId(), firebaseApp.getOptions().getProjectId(), - persistedInstallationEntry.getRefreshToken()); + entry.getRefreshToken()); } catch (FirebaseException exception) { throw new FirebaseInstallationsException( - "Failed to delete a Firebase Installation.", - FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); + "Failed to delete a Firebase Installation.", Status.BAD_CONFIG); } } - persistedInstallation.clear(); + persistedInstallation.insertOrUpdatePersistedInstallationEntry(entry.withNoGeneratedFid()); return null; } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java index 4309a782ffd..07683203570 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallationsException.java @@ -19,13 +19,12 @@ /** The class for all Exceptions thrown by {@link FirebaseInstallations}. */ public class FirebaseInstallationsException extends FirebaseException { - // TODO(ankitagj): Improve exception handling and java doc public enum Status { - SDK_INTERNAL_ERROR, - - CLIENT_ERROR, - - AUTHENTICATION_ERROR + /** + * Indicates that the caller is misconfigured, usually with a bad or misconfigured API Key or + * Project. + */ + BAD_CONFIG, } @NonNull private final Status status; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java index bc36a20bac6..2c067a30507 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetAuthTokenListener.java @@ -28,12 +28,10 @@ public GetAuthTokenListener( } @Override - public boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken) { + public boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry) { // AuthTokenListener state is reached when FID is registered and has a valid auth token if (persistedInstallationEntry.isRegistered() - && !utils.isAuthTokenExpired(persistedInstallationEntry) - && !shouldRefreshAuthToken) { + && !utils.isAuthTokenExpired(persistedInstallationEntry)) { resultTaskCompletionSource.setResult( InstallationTokenResult.builder() .setToken(persistedInstallationEntry.getAuthToken()) @@ -48,7 +46,9 @@ public boolean onStateReached( @Override public boolean onException( PersistedInstallationEntry persistedInstallationEntry, Exception exception) { - if (persistedInstallationEntry.isErrored() || persistedInstallationEntry.isNotGenerated()) { + if (persistedInstallationEntry.isErrored() + || persistedInstallationEntry.isNotGenerated() + || persistedInstallationEntry.isUnregistered()) { resultTaskCompletionSource.trySetException(exception); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java index 38134cc6217..44f534f7da1 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/GetIdListener.java @@ -25,9 +25,10 @@ public GetIdListener(TaskCompletionSource taskCompletionSource) { } @Override - public boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean unused) { - if (persistedInstallationEntry.isUnregistered() || persistedInstallationEntry.isRegistered()) { + public boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry) { + if (persistedInstallationEntry.isUnregistered() + || persistedInstallationEntry.isRegistered() + || persistedInstallationEntry.isErrored()) { taskCompletionSource.trySetResult(persistedInstallationEntry.getFirebaseInstallationId()); return true; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java new file mode 100644 index 00000000000..9931d20045c --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/RandomFidGenerator.java @@ -0,0 +1,84 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.UUID; + +public class RandomFidGenerator { + /** + * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this + * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy + * Instance-IDs + */ + private static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); + + /** + * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND + * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. + */ + private static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); + + /** Length of new-format FIDs as introduced in 2019. */ + private static final int FID_LENGTH = 22; + + /** + * Creates a random FID of valid format without checking if the FID is already in use by any + * Firebase Installation. + * + *

Note: Even though this method does not check with the FIS database if the returned FID is + * already in use, the probability of collision is extremely and negligibly small! + * + * @return random FID value + */ + @NonNull + public String createRandomFid() { + // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. + byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); + uuidBytes[16] = uuidBytes[0]; + uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); + return encodeFidBase64UrlSafe(uuidBytes); + } + + /** + * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded + * String-representation. + * + *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is + * recommended to deliver a byte-array containing at least 16.5 bytes. + * + * @param rawValue FID value to be encoded + * @return (22-character or shorter) String containing the base64-encoded value + */ + private static String encodeFidBase64UrlSafe(byte[] rawValue) { + return new String( + android.util.Base64.encode( + rawValue, + android.util.Base64.URL_SAFE + | android.util.Base64.NO_PADDING + | android.util.Base64.NO_WRAP), + Charset.defaultCharset()) + .substring(0, FID_LENGTH); + } + + private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { + ByteBuffer bb = ByteBuffer.wrap(output); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java index d9570835adf..8cd08bdb299 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/StateListener.java @@ -21,8 +21,7 @@ interface StateListener { * Returns {@code true} if the defined {@link PersistedInstallationEntry} state is reached, {@code * false} otherwise. */ - boolean onStateReached( - PersistedInstallationEntry persistedInstallationEntry, boolean shouldRefreshAuthToken); + boolean onStateReached(PersistedInstallationEntry persistedInstallationEntry); /** * Returns {@code true} if an exception is thrown while registering a Firebase Installation, diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java index 22bf2576ea5..d2f748ce734 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/Utils.java @@ -14,99 +14,37 @@ package com.google.firebase.installations; -import androidx.annotation.NonNull; -import com.google.android.gms.common.util.Clock; +import android.text.TextUtils; import com.google.firebase.installations.local.PersistedInstallationEntry; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.UUID; +import java.util.Calendar; import java.util.concurrent.TimeUnit; /** Util methods used for {@link FirebaseInstallations} */ class Utils { + public static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1); + private final Calendar calendar; - private final Clock clock; - /** - * 1 Byte with the first 4 header-bits set to the identifying FID prefix 0111 (0x7). Use this - * constant to create FIDs or check the first byte of FIDs. This prefix is also used in legacy - * Instance-IDs - */ - public static final byte FID_4BIT_PREFIX = Byte.parseByte("01110000", 2); - - /** - * Byte mask to remove the 4 header-bits of a given Byte. Use this constant with Java's Binary AND - * Operator in order to remove the first 4 bits of a Byte and replacing it with the FID prefix. - */ - public static final byte REMOVE_PREFIX_MASK = Byte.parseByte("00001111", 2); - - /** Length of new-format FIDs as introduced in 2019. */ - public static final int FID_LENGTH = 22; - - private static final long AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS = TimeUnit.HOURS.toSeconds(1); - - Utils(Clock clock) { - this.clock = clock; + Utils(Calendar calendar) { + this.calendar = calendar; } /** * Checks if the FIS Auth token is expired or going to expire in next 1 hour {@link * #AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS}. */ - public boolean isAuthTokenExpired(PersistedInstallationEntry persistedInstallationEntry) { - return persistedInstallationEntry.isRegistered() - && persistedInstallationEntry.getTokenCreationEpochInSecs() - + persistedInstallationEntry.getExpiresInSecs() - < currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS; + public boolean isAuthTokenExpired(PersistedInstallationEntry entry) { + if (TextUtils.isEmpty(entry.getAuthToken())) { + return true; + } + if ((entry.getTokenCreationEpochInSecs() + entry.getExpiresInSecs()) + < (currentTimeInSecs() + AUTH_TOKEN_EXPIRATION_BUFFER_IN_SECS)) { + return true; + } + return false; } /** Returns current time in seconds. */ public long currentTimeInSecs() { - return TimeUnit.MILLISECONDS.toSeconds(clock.currentTimeMillis()); - } - - /** - * Creates a random FID of valid format without checking if the FID is already in use by any - * Firebase Installation. - * - *

Note: Even though this method does not check with the FIS database if the returned FID is - * already in use, the probability of collision is extremely and negligibly small! - * - * @return random FID value - */ - @NonNull - public String createRandomFid() { - // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 bytes. - byte[] uuidBytes = getBytesFromUUID(UUID.randomUUID(), new byte[17]); - uuidBytes[16] = uuidBytes[0]; - uuidBytes[0] = (byte) ((REMOVE_PREFIX_MASK & uuidBytes[0]) | FID_4BIT_PREFIX); - return encodeFidBase64UrlSafe(uuidBytes); - } - - /** - * Converts a given byte-array (assumed to be an FID value) to base64-url-safe encoded - * String-representation. - * - *

Note: The returned String has at most 22 characters, the length of FIDs. Thus, it is - * recommended to deliver a byte-array containing at least 16.5 bytes. - * - * @param rawValue FID value to be encoded - * @return (22-character or shorter) String containing the base64-encoded value - */ - private static String encodeFidBase64UrlSafe(byte[] rawValue) { - return new String( - android.util.Base64.encode( - rawValue, - android.util.Base64.URL_SAFE - | android.util.Base64.NO_PADDING - | android.util.Base64.NO_WRAP), - Charset.defaultCharset()) - .substring(0, FID_LENGTH); - } - - private static byte[] getBytesFromUUID(UUID uuid, byte[] output) { - ByteBuffer bb = ByteBuffer.wrap(output); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - return bb.array(); + return TimeUnit.MILLISECONDS.toSeconds(calendar.getTimeInMillis()); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java index fdee274945d..d3786c4b6e4 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallation.java @@ -14,23 +14,33 @@ package com.google.firebase.installations.local; -import android.content.Context; -import android.content.SharedPreferences; -import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; -import java.util.Arrays; -import java.util.List; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import org.json.JSONException; +import org.json.JSONObject; /** * A layer that locally persists a few Firebase Installation attributes on top the Firebase * Installation API. */ public class PersistedInstallation { + private final File dataFile; + @NonNull private final FirebaseApp firebaseApp; + // Registration Status of each persisted fid entry - // NOTE: never change the ordinal of the enum values because the enum values are stored in shared - // prefs as their ordinal numbers. + // NOTE: never change the ordinal of the enum values because the enum values are written to + // local storage as their ordinal numbers. public enum RegistrationStatus { + /** + * {@link PersistedInstallationEntry} legacy registration status. Next state: UNREGISTERED - A + * new FID is created and persisted locally before registering with FIS servers. + */ + ATTEMPT_MIGRATION, /** * {@link PersistedInstallationEntry} default registration status. Next state: UNREGISTERED - A * new FID is created and persisted locally before registering with FIS servers. @@ -55,8 +65,7 @@ public enum RegistrationStatus { REGISTER_ERROR, } - private static final String SHARED_PREFS_NAME = "PersistedInstallation"; - + private static final String SETTINGS_FILE_NAME_PREFIX = "PersistedInstallation"; private static final String FIREBASE_INSTALLATION_ID_KEY = "Fid"; private static final String AUTH_TOKEN_KEY = "AuthToken"; private static final String REFRESH_TOKEN_KEY = "RefreshToken"; @@ -65,94 +74,103 @@ public enum RegistrationStatus { private static final String PERSISTED_STATUS_KEY = "Status"; private static final String FIS_ERROR_KEY = "FisError"; - private static final List FID_PREF_KEYS = - Arrays.asList( - FIREBASE_INSTALLATION_ID_KEY, - AUTH_TOKEN_KEY, - REFRESH_TOKEN_KEY, - TOKEN_CREATION_TIME_IN_SECONDS_KEY, - EXPIRES_IN_SECONDS_KEY, - PERSISTED_STATUS_KEY, - FIS_ERROR_KEY); - - @GuardedBy("prefs") - private final SharedPreferences prefs; - - private final String persistenceKey; - public PersistedInstallation(@NonNull FirebaseApp firebaseApp) { // Different FirebaseApp in the same Android application should have the same application // context and same dir path - prefs = - firebaseApp - .getApplicationContext() - .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - persistenceKey = firebaseApp.getPersistenceKey(); + dataFile = + new File( + firebaseApp.getApplicationContext().getFilesDir(), + SETTINGS_FILE_NAME_PREFIX + "." + firebaseApp.getPersistenceKey() + ".json"); + this.firebaseApp = firebaseApp; } @NonNull public PersistedInstallationEntry readPersistedInstallationEntryValue() { - synchronized (prefs) { - String fid = prefs.getString(getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(PERSISTED_STATUS_KEY), -1); - String authToken = prefs.getString(getSharedPreferencesKey(AUTH_TOKEN_KEY), null); - String refreshToken = prefs.getString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), null); - long tokenCreationTime = - prefs.getLong(getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), 0); - long expiresIn = prefs.getLong(getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), 0); - String fisError = prefs.getString(getSharedPreferencesKey(FIS_ERROR_KEY), null); + JSONObject json = readJSONFromFile(); - if (fid == null || !(status >= 0 && status < RegistrationStatus.values().length)) { - return PersistedInstallationEntry.builder().build(); - } - return PersistedInstallationEntry.builder() - .setFirebaseInstallationId(fid) - .setRegistrationStatus(RegistrationStatus.values()[status]) - .setAuthToken(authToken) - .setRefreshToken(refreshToken) - .setTokenCreationEpochInSecs(tokenCreationTime) - .setExpiresInSecs(expiresIn) - .setFisError(fisError) - .build(); - } + String fid = json.optString(FIREBASE_INSTALLATION_ID_KEY, null); + int status = json.optInt(PERSISTED_STATUS_KEY, RegistrationStatus.ATTEMPT_MIGRATION.ordinal()); + String authToken = json.optString(AUTH_TOKEN_KEY, null); + String refreshToken = json.optString(REFRESH_TOKEN_KEY, null); + long tokenCreationTime = json.optLong(TOKEN_CREATION_TIME_IN_SECONDS_KEY, 0); + long expiresIn = json.optLong(EXPIRES_IN_SECONDS_KEY, 0); + String fisError = json.optString(FIS_ERROR_KEY, null); + + PersistedInstallationEntry prefs = + PersistedInstallationEntry.builder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.values()[status]) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setTokenCreationEpochInSecs(tokenCreationTime) + .setExpiresInSecs(expiresIn) + .setFisError(fisError) + .build(); + return prefs; } - @NonNull - public boolean insertOrUpdatePersistedInstallationEntry( - @NonNull PersistedInstallationEntry entryValue) { - synchronized (prefs) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString( - getSharedPreferencesKey(FIREBASE_INSTALLATION_ID_KEY), - entryValue.getFirebaseInstallationId()); - editor.putInt( - getSharedPreferencesKey(PERSISTED_STATUS_KEY), - entryValue.getRegistrationStatus().ordinal()); - editor.putString(getSharedPreferencesKey(AUTH_TOKEN_KEY), entryValue.getAuthToken()); - editor.putString(getSharedPreferencesKey(REFRESH_TOKEN_KEY), entryValue.getRefreshToken()); - editor.putLong( - getSharedPreferencesKey(TOKEN_CREATION_TIME_IN_SECONDS_KEY), - entryValue.getTokenCreationEpochInSecs()); - editor.putLong( - getSharedPreferencesKey(EXPIRES_IN_SECONDS_KEY), entryValue.getExpiresInSecs()); - editor.putString(getSharedPreferencesKey(FIS_ERROR_KEY), entryValue.getFisError()); - return editor.commit(); + private JSONObject readJSONFromFile() { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] tmpBuf = new byte[16 * 1024]; + try (FileInputStream fis = new FileInputStream(dataFile)) { + while (true) { + int numRead = fis.read(tmpBuf, 0, tmpBuf.length); + if (numRead < 0) { + break; + } + baos.write(tmpBuf, 0, numRead); + } + return new JSONObject(baos.toString()); + } catch (IOException | JSONException e) { + return new JSONObject(); } } + /** + * Write the prefs to a JSON object, serialize them into a JSON string and write the bytes to a + * temp file. After writing and closing the temp file, rename it over to the actual + * SETTINGS_FILE_NAME. + */ @NonNull - public boolean clear() { - synchronized (prefs) { - SharedPreferences.Editor editor = prefs.edit(); - for (String k : FID_PREF_KEYS) { - editor.remove(getSharedPreferencesKey(k)); + public PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry( + @NonNull PersistedInstallationEntry prefs) { + try { + // Write the prefs into a JSON object + JSONObject json = new JSONObject(); + json.put(FIREBASE_INSTALLATION_ID_KEY, prefs.getFirebaseInstallationId()); + json.put(PERSISTED_STATUS_KEY, prefs.getRegistrationStatus().ordinal()); + json.put(AUTH_TOKEN_KEY, prefs.getAuthToken()); + json.put(REFRESH_TOKEN_KEY, prefs.getRefreshToken()); + json.put(TOKEN_CREATION_TIME_IN_SECONDS_KEY, prefs.getTokenCreationEpochInSecs()); + json.put(EXPIRES_IN_SECONDS_KEY, prefs.getExpiresInSecs()); + json.put(FIS_ERROR_KEY, prefs.getFisError()); + File tmpFile = + File.createTempFile( + SETTINGS_FILE_NAME_PREFIX, "tmp", firebaseApp.getApplicationContext().getFilesDir()); + + // Werialize the JSON object into a string and write the bytes to a temp file + FileOutputStream fos = new FileOutputStream(tmpFile); + fos.write(json.toString().getBytes("UTF-8")); + fos.close(); + + // Snapshot the temp file to the actual file + if (!tmpFile.renameTo(dataFile)) { + throw new IOException("unable to rename the tmpfile to " + SETTINGS_FILE_NAME_PREFIX); } - editor.commit(); - return editor.commit(); + } catch (JSONException | IOException e) { + // This should only happen when the storage is full or the system is corrupted. + // There isn't a lot we can do when this happens, other than crash the process. It is a + // bit nicer to eat the error and hope that the user clears some storage space on their + // device. } + + // Return the prefs that were written to make it easy for the caller to use them in a + // future step (e.g. for chaining calls). + return prefs; } - private String getSharedPreferencesKey(String key) { - return String.format("%s|%s", persistenceKey, key); + /** Sets the state to ATTEMPT_MIGRATION. */ + public void clearForTesting() { + dataFile.delete(); } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java index aa2ffee2e58..f58f49a6afb 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/PersistedInstallationEntry.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; /** * This class represents a persisted fid entry in {@link PersistedInstallation}, which contains a @@ -44,6 +45,9 @@ public abstract class PersistedInstallationEntry { @Nullable public abstract String getFisError(); + @NonNull + public static PersistedInstallationEntry INSTANCE = PersistedInstallationEntry.builder().build(); + public boolean isRegistered() { return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.REGISTERED; } @@ -57,7 +61,65 @@ public boolean isUnregistered() { } public boolean isNotGenerated() { - return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED; + return getRegistrationStatus() == PersistedInstallation.RegistrationStatus.NOT_GENERATED + || getRegistrationStatus() == RegistrationStatus.ATTEMPT_MIGRATION; + } + + public boolean shouldAttemptMigration() { + return getRegistrationStatus() == RegistrationStatus.ATTEMPT_MIGRATION; + } + + @NonNull + public PersistedInstallationEntry withUnregisteredFid(@NonNull String fid) { + return toBuilder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.UNREGISTERED) + .build(); + } + + @NonNull + public PersistedInstallationEntry withRegisteredFid( + @NonNull String fid, + @NonNull String refreshToken, + long creationTime, + @Nullable String authToken, + long authTokenExpiration) { + return toBuilder() + .setFirebaseInstallationId(fid) + .setRegistrationStatus(RegistrationStatus.REGISTERED) + .setAuthToken(authToken) + .setRefreshToken(refreshToken) + .setExpiresInSecs(authTokenExpiration) + .setTokenCreationEpochInSecs(creationTime) + .build(); + } + + @NonNull + public PersistedInstallationEntry withFisError(@NonNull String message) { + return toBuilder() + .setFisError(message) + .setRegistrationStatus(RegistrationStatus.REGISTER_ERROR) + .build(); + } + + @NonNull + public PersistedInstallationEntry withNoGeneratedFid() { + return toBuilder().setRegistrationStatus(RegistrationStatus.NOT_GENERATED).build(); + } + + @NonNull + public PersistedInstallationEntry withAuthToken( + @NonNull String authToken, long authTokenExpiration, long creationTime) { + return toBuilder() + .setAuthToken(authToken) + .setExpiresInSecs(authTokenExpiration) + .setTokenCreationEpochInSecs(creationTime) + .build(); + } + + @NonNull + public PersistedInstallationEntry withClearedAuthToken() { + return toBuilder().setAuthToken(null).build(); } @NonNull @@ -68,7 +130,7 @@ public boolean isNotGenerated() { public static PersistedInstallationEntry.Builder builder() { return new AutoValue_PersistedInstallationEntry.Builder() .setTokenCreationEpochInSecs(0) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.NOT_GENERATED) + .setRegistrationStatus(RegistrationStatus.ATTEMPT_MIGRATION) .setExpiresInSecs(0); } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index a981635c02c..bb392f9472e 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -29,6 +29,8 @@ import com.google.firebase.FirebaseException; import com.google.firebase.heartbeatinfo.HeartBeatInfo; import com.google.firebase.heartbeatinfo.HeartBeatInfo.HeartBeat; +import com.google.firebase.installations.FirebaseInstallationsException; +import com.google.firebase.installations.FirebaseInstallationsException.Status; import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.BufferedReader; @@ -65,9 +67,6 @@ public class FirebaseInstallationServiceClient { private static final String HEART_BEAT_HEADER = "x-firebase-client-log-type"; private static final String USER_AGENT_HEADER = "x-firebase-client"; - private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; - private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error: %s"; - private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; @@ -102,57 +101,60 @@ public FirebaseInstallationServiceClient( * @param projectID Project Id * @param appId the identifier of a Firebase application * @return {@link InstallationResponse} generated from the response body + *

    + *
  • 400: return response with status BAD_CONFIG + *
  • 403: return response with status BAD_CONFIG + *
  • 403: return response with status BAD_CONFIG + *
  • 429: throw IOException + *
  • 500: throw IOException + *
*/ @NonNull public InstallationResponse createFirebaseInstallation( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) - throws FirebaseException { + throws IOException { String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); - try { - int retryCount = 0; - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - while (retryCount <= MAX_RETRIES) { - HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.setDoOutput(true); - - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } + int retryCount = 0; + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.setDoOutput(true); + + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } - int httpResponseCode = httpsURLConnection.getResponseCode(); + int httpResponseCode = httpsURLConnection.getResponseCode(); - if (httpResponseCode == 200) { - return readCreateResponse(httpsURLConnection); - } - // Usually the FIS server recovers from errors: retry one time before giving up. - if (httpResponseCode >= 500 && httpResponseCode < 600) { - retryCount++; - continue; - } + if (httpResponseCode == 200) { + return readCreateResponse(httpsURLConnection); + } - // Unrecoverable server response or unknown error - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; } - // Return empty installation response with SERVER_ERROR response code after max retries - return InstallationResponse.builder().setResponseCode(ResponseCode.SERVER_ERROR).build(); - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + // Return empty installation response with BAD_CONFIG response code after max retries + return InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build(); } + + throw new IOException(); } private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, String appId) @@ -161,6 +163,7 @@ private static JSONObject buildCreateFirebaseInstallationRequestBody(String fid, firebaseInstallationData.put("fid", fid); firebaseInstallationData.put("appId", appId); firebaseInstallationData.put("authVersion", FIREBASE_INSTALLATION_AUTH_VERSION); + firebaseInstallationData.put("sdkVersion", "t.1.1.0"); return firebaseInstallationData; } @@ -178,32 +181,39 @@ public void deleteFirebaseInstallation( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseException { + throws FirebaseException, IOException { String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); - try { - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + + int retryCount = 0; + while (retryCount <= MAX_RETRIES) { HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); httpsURLConnection.setRequestMethod("DELETE"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return; - default: - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + + if (httpResponseCode == 200 || httpResponseCode == 401 || httpResponseCode == 404) { + return; } - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; + } + + throw new FirebaseInstallationsException( + "bad config while trying to delete FID", Status.BAD_CONFIG); } + + throw new IOException(); } /** @@ -214,6 +224,14 @@ public void deleteFirebaseInstallation( * @param fid Firebase Installation Identifier * @param projectID Project Id * @param refreshToken a token used to authenticate FIS requests + *
    + *
  • 400: return response with status BAD_CONFIG + *
  • 401: return response with status INVALID_AUTH + *
  • 403: return response with status BAD_CONFIG + *
  • 404: return response with status INVALID_AUTH + *
  • 429: throw IOException + *
  • 500: throw IOException + *
*/ @NonNull public TokenResult generateAuthToken( @@ -221,53 +239,41 @@ public TokenResult generateAuthToken( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseException { + throws IOException { String resourceName = String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); - try { - int retryCount = 0; - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_INSTALLATIONS_API_DOMAIN, - FIREBASE_INSTALLATIONS_API_VERSION, - resourceName, - apiKey)); - while (retryCount <= MAX_RETRIES) { - HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - - int httpResponseCode = httpsURLConnection.getResponseCode(); - - if (httpResponseCode == 200) { - return readGenerateAuthTokenResponse(httpsURLConnection); - } + int retryCount = 0; + URL url = + new URL( + String.format( + "https://%s/%s/%s?key=%s", + FIREBASE_INSTALLATIONS_API_DOMAIN, + FIREBASE_INSTALLATIONS_API_VERSION, + resourceName, + apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - if (httpResponseCode == 401) { - return TokenResult.builder() - .setResponseCode(TokenResult.ResponseCode.REFRESH_TOKEN_ERROR) - .build(); - } + int httpResponseCode = httpsURLConnection.getResponseCode(); - if (httpResponseCode == 404) { - return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.FID_ERROR).build(); - } + if (httpResponseCode == 200) { + return readGenerateAuthTokenResponse(httpsURLConnection); + } - // Usually the FIS server recovers from errors: retry one time before giving up. - if (httpResponseCode >= 500 && httpResponseCode < 600) { - retryCount++; - continue; - } + if (httpResponseCode == 401 || httpResponseCode == 404) { + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build(); + } - // Unrecoverable server response or unknown error - throw new FirebaseException(readErrorResponse(httpsURLConnection)); + if (httpResponseCode == 429 || (httpResponseCode >= 500 && httpResponseCode < 600)) { + retryCount++; + continue; } - throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); - } catch (IOException e) { - throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); + + return TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build(); } + throw new IOException(); } private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java index 0c0463b6f49..213b4d19416 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/InstallationResponse.java @@ -24,8 +24,9 @@ public abstract class InstallationResponse { public enum ResponseCode { // Returned on success OK, - // An error occurred on the server while processing this request(temporary) - SERVER_ERROR + // The request is invalid. Do not try again without fixing the request. Usually means + // a bad or misconfigured API Key or project is being used. + BAD_CONFIG, } @Nullable diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java index a120e418ff0..1fd7a5d99fd 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/TokenResult.java @@ -27,14 +27,10 @@ public enum ResponseCode { OK, // Auth token cannot be generated for this FID in the request. Because it is not // registered/found on the FIS server. Recreate a new fid to fetch a valid auth token. - FID_ERROR, + BAD_CONFIG, // Refresh token in this request in not accepted by the FIS server. Either it has been blocked // or changed. Recreate a new fid to fetch a valid auth token. - REFRESH_TOKEN_ERROR, - } - - public boolean isSuccessful() { - return getResponseCode() == ResponseCode.OK; + AUTH_ERROR, } /** A new FIS Auth-Token, created for this Firebase Installation. */ From 7d91146397fd858bc11946b6ad96dfc019fb6c9c Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 9 Dec 2019 17:37:30 -0800 Subject: [PATCH 67/74] Remove FirebaseSegmentation from fis_sdk branch. (was added by mistake) --- .../firebase-segmentation.gradle | 70 ----- firebase-segmentation/gradle.properties | 1 - firebase-segmentation/lint.xml | 11 - .../src/androidTest/AndroidManifest.xml | 26 -- .../FirebaseSegmentationInstrumentedTest.java | 260 ----------------- .../local/CustomInstallationIdCacheTest.java | 89 ------ .../src/main/AndroidManifest.xml | 29 -- .../segmentation/FirebaseSegmentation.java | 266 ------------------ .../FirebaseSegmentationRegistrar.java | 38 --- .../SetCustomInstallationIdException.java | 68 ----- .../google/firebase/segmentation/Utils.java | 33 --- .../local/CustomInstallationIdCache.java | 98 ------- .../CustomInstallationIdCacheEntryValue.java | 44 --- .../remote/SegmentationServiceClient.java | 219 -------------- .../FirebaseSegmentationRegistrarTest.java | 54 ---- subprojects.cfg | 1 - 16 files changed, 1307 deletions(-) delete mode 100644 firebase-segmentation/firebase-segmentation.gradle delete mode 100644 firebase-segmentation/gradle.properties delete mode 100644 firebase-segmentation/lint.xml delete mode 100644 firebase-segmentation/src/androidTest/AndroidManifest.xml delete mode 100644 firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java delete mode 100644 firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java delete mode 100644 firebase-segmentation/src/main/AndroidManifest.xml delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java delete mode 100644 firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java delete mode 100644 firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle deleted file mode 100644 index c11085bd161..00000000000 --- a/firebase-segmentation/firebase-segmentation.gradle +++ /dev/null @@ -1,70 +0,0 @@ -// 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. - -plugins { - id 'firebase-library' -} - -firebaseLibrary { - testLab.enabled = true -} - -android { - compileSdkVersion project.targetSdkVersion - - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - multiDexEnabled true - versionName version - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - implementation project(':firebase-common') - - implementation('com.google.firebase:firebase-iid:17.0.3') { - exclude group: "com.google.firebase", module: "firebase-common" - } - - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.multidex:multidex:2.0.1' - implementation 'com.google.android.gms:play-services-tasks:17.0.0' - - 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.0.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" - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' -} diff --git a/firebase-segmentation/gradle.properties b/firebase-segmentation/gradle.properties deleted file mode 100644 index 752913a3eb5..00000000000 --- a/firebase-segmentation/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -version=17.1.1 diff --git a/firebase-segmentation/lint.xml b/firebase-segmentation/lint.xml deleted file mode 100644 index 9c521180b8f..00000000000 --- a/firebase-segmentation/lint.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/firebase-segmentation/src/androidTest/AndroidManifest.xml b/firebase-segmentation/src/androidTest/AndroidManifest.xml deleted file mode 100644 index f3ec53d62a2..00000000000 --- a/firebase-segmentation/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java deleted file mode 100644 index 034ee1eb339..00000000000 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ /dev/null @@ -1,260 +0,0 @@ -// 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.assertNull; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.when; - -import androidx.annotation.NonNull; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.InstanceIdResult; -import com.google.firebase.segmentation.local.CustomInstallationIdCache; -import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; -import com.google.firebase.segmentation.remote.SegmentationServiceClient; -import java.util.concurrent.ExecutionException; -import org.junit.After; -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.MethodSorters; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class FirebaseSegmentationInstrumentedTest { - - private static final String CUSTOM_INSTALLATION_ID = "123"; - private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; - - private FirebaseApp firebaseApp; - @Mock private FirebaseInstanceId firebaseInstanceId; - @Mock private SegmentationServiceClient backendClientReturnsOk; - @Mock private SegmentationServiceClient backendClientReturnsError; - private CustomInstallationIdCache actualCache; - @Mock private CustomInstallationIdCache cacheReturnsError; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - FirebaseApp.clearInstancesForTest(); - firebaseApp = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder() - .setApplicationId("1:123456789:android:abcdef") - .setApiKey("api_key") - .build()); - actualCache = new CustomInstallationIdCache(firebaseApp); - - when(backendClientReturnsOk.updateCustomInstallationId( - anyLong(), anyString(), anyString(), anyString(), anyString())) - .thenReturn(SegmentationServiceClient.Code.OK); - when(backendClientReturnsOk.clearCustomInstallationId( - anyLong(), anyString(), anyString(), anyString())) - .thenReturn(SegmentationServiceClient.Code.OK); - when(backendClientReturnsError.updateCustomInstallationId( - anyLong(), anyString(), anyString(), anyString(), anyString())) - .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); - when(backendClientReturnsError.clearCustomInstallationId( - anyLong(), anyString(), anyString(), anyString())) - .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); - when(firebaseInstanceId.getInstanceId()) - .thenReturn( - Tasks.forResult( - new InstanceIdResult() { - @NonNull - @Override - public String getId() { - return FIREBASE_INSTANCE_ID; - } - - @NonNull - @Override - public String getToken() { - return "iid_token"; - } - })); - when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false); - when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); - } - - @After - public void cleanUp() throws Exception { - actualCache.clear(); - } - - @Test - public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); - - // No exception, means success. - assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID))); - CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); - assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); - assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); - } - - @Test - public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() - throws InterruptedException { - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); - - // Expect exception - try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); - fail(); - } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); - assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); - assertThat(((SetCustomInstallationIdException) cause).getStatus()) - .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); - } - - CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); - assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); - assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); - } - - @Test - public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() - throws InterruptedException { - when(backendClientReturnsError.updateCustomInstallationId( - anyLong(), anyString(), anyString(), anyString(), anyString())) - .thenReturn(SegmentationServiceClient.Code.CONFLICT); - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); - - // Expect exception - try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); - fail(); - } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); - assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); - assertThat(((SetCustomInstallationIdException) cause).getStatus()) - .isEqualTo(SetCustomInstallationIdException.Status.DUPLICATED_CUSTOM_INSTALLATION_ID); - } - - CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); - assertThat(entryValue).isNull(); - } - - @Test - public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); - - // Expect exception - try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); - fail(); - } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); - assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); - assertThat(((SetCustomInstallationIdException) cause).getStatus()) - .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); - } - } - - @Test - public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED)); - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); - - // No exception, means success. - assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(null))); - CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); - assertNull(entryValue); - } - - @Test - public void testClearCustomInstallationId_CacheOk_BackendError() throws Exception { - actualCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, - CustomInstallationIdCache.CacheStatus.SYNCED)); - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); - - // Expect exception - try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(null)); - fail(); - } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); - assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); - assertThat(((SetCustomInstallationIdException) cause).getStatus()) - .isEqualTo(SetCustomInstallationIdException.Status.BACKEND_ERROR); - } - - CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); - assertThat(entryValue.getCustomInstallationId().isEmpty()).isTrue(); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); - assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_CLEAR); - } - - @Test - public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { - FirebaseSegmentation firebaseSegmentation = - new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); - - // Expect exception - try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); - fail(); - } catch (ExecutionException expected) { - Throwable cause = expected.getCause(); - assertThat(cause).isInstanceOf(SetCustomInstallationIdException.class); - assertThat(((SetCustomInstallationIdException) cause).getStatus()) - .isEqualTo(SetCustomInstallationIdException.Status.CLIENT_ERROR); - } - } -} diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java deleted file mode 100644 index 9e22e522d2b..00000000000 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ /dev/null @@ -1,89 +0,0 @@ -// 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.local; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import androidx.test.core.app.ApplicationProvider; -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 cache0; - private CustomInstallationIdCache cache1; - - @Before - public void setUp() { - FirebaseApp.clearInstancesForTest(); - firebaseApp0 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); - firebaseApp1 = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), - "firebase_app_1"); - cache0 = new CustomInstallationIdCache(firebaseApp0); - cache1 = new CustomInstallationIdCache(firebaseApp1); - } - - @After - public void cleanUp() throws Exception { - cache0.clear(); - cache1.clear(); - } - - @Test - public void testReadCacheEntry_Null() { - assertNull(cache0.readCacheEntryValue()); - assertNull(cache1.readCacheEntryValue()); - } - - @Test - public void testUpdateAndReadCacheEntry() throws Exception { - assertTrue( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING_UPDATE))); - CustomInstallationIdCacheEntryValue entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); - assertThat(entryValue.getCacheStatus()) - .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); - assertNull(cache1.readCacheEntryValue()); - - assertTrue( - cache0.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED))); - entryValue = cache0.readCacheEntryValue(); - assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); - assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); - } -} diff --git a/firebase-segmentation/src/main/AndroidManifest.xml b/firebase-segmentation/src/main/AndroidManifest.xml deleted file mode 100644 index 9005c369f32..00000000000 --- a/firebase-segmentation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java deleted file mode 100644 index b1d5e6e6742..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentation.java +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.segmentation; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; -import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.InstanceIdResult; -import com.google.firebase.segmentation.SetCustomInstallationIdException.Status; -import com.google.firebase.segmentation.local.CustomInstallationIdCache; -import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; -import com.google.firebase.segmentation.remote.SegmentationServiceClient; -import com.google.firebase.segmentation.remote.SegmentationServiceClient.Code; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -/** Entry point of Firebase Segmentation SDK. */ -public class FirebaseSegmentation { - - public static final String TAG = "FirebaseSegmentation"; - - private final FirebaseApp firebaseApp; - private final FirebaseInstanceId firebaseInstanceId; - private final CustomInstallationIdCache localCache; - private final SegmentationServiceClient backendServiceClient; - private final Executor executor; - - FirebaseSegmentation(FirebaseApp firebaseApp) { - this( - firebaseApp, - FirebaseInstanceId.getInstance(firebaseApp), - new CustomInstallationIdCache(firebaseApp), - new SegmentationServiceClient(firebaseApp.getApplicationContext())); - } - - FirebaseSegmentation( - FirebaseApp firebaseApp, - FirebaseInstanceId firebaseInstanceId, - CustomInstallationIdCache localCache, - SegmentationServiceClient backendServiceClient) { - this.firebaseApp = firebaseApp; - this.firebaseInstanceId = firebaseInstanceId; - this.localCache = localCache; - this.backendServiceClient = backendServiceClient; - this.executor = Executors.newFixedThreadPool(4); - } - - /** - * Returns the {@link FirebaseSegmentation} initialized with the default {@link FirebaseApp}. - * - * @return a {@link FirebaseSegmentation} instance - */ - @NonNull - public static FirebaseSegmentation getInstance() { - FirebaseApp defaultFirebaseApp = FirebaseApp.getInstance(); - return getInstance(defaultFirebaseApp); - } - - /** - * Returns the {@link FirebaseSegmentation} initialized with a custom {@link FirebaseApp}. - * - * @param app a custom {@link FirebaseApp} - * @return a {@link FirebaseSegmentation} instance - */ - @NonNull - public static FirebaseSegmentation getInstance(@NonNull FirebaseApp app) { - Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp."); - return app.get(FirebaseSegmentation.class); - } - - @NonNull - public synchronized Task setCustomInstallationId(@Nullable String customInstallationId) { - if (customInstallationId == null) { - return Tasks.call(executor, () -> clearCustomInstallationId()); - } - return Tasks.call(executor, () -> updateCustomInstallationId(customInstallationId)); - } - - /** - * Update custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and - * client side cache. - * - *
-   *     The workflow is:
-   *         check diff against cache or cache status is not SYNCED
-   *                                 |
-   *                  get Firebase instance id and token
-   *                      |                       |
-   *                      |      update cache with cache status PENDING_UPDATE
-   *                      |                       |
-   *                    send http request to backend
-   *                                 |
-   *              on success: set cache entry status to SYNCED
-   *                                 |
-   *                               return
-   * 
- */ - @WorkerThread - private Void updateCustomInstallationId(String customInstallationId) - throws SetCustomInstallationIdException { - CustomInstallationIdCacheEntryValue cacheEntryValue = localCache.readCacheEntryValue(); - if (cacheEntryValue != null - && cacheEntryValue.getCustomInstallationId().equals(customInstallationId) - && cacheEntryValue.getCacheStatus() == CustomInstallationIdCache.CacheStatus.SYNCED) { - // If the given custom installation id matches up the cached - // value, there's no need to update. - return null; - } - - InstanceIdResult instanceIdResult; - try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); - } catch (ExecutionException | InterruptedException e) { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to get Firebase instance id"); - } - - boolean firstUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); - - if (!firstUpdateCacheResult) { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to update client side cache"); - } - - // Start requesting backend when first cache updae is done. - String iid = instanceIdResult.getId(); - String iidToken = instanceIdResult.getToken(); - Code backendRequestResult = - backendServiceClient.updateCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - customInstallationId, - iid, - iidToken); - - boolean finalUpdateCacheResult; - switch (backendRequestResult) { - case OK: - finalUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.SYNCED)); - break; - case UNAUTHORIZED: - localCache.clear(); - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Instance id token is invalid."); - case CONFLICT: - localCache.clear(); - throw new SetCustomInstallationIdException( - Status.DUPLICATED_CUSTOM_INSTALLATION_ID, - "The custom installation id is used by another Firebase installation in your project."); - case HTTP_CLIENT_ERROR: - localCache.clear(); - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); - case NETWORK_ERROR: - case SERVER_ERROR: - default: - // These are considered retryable errors, so not to clean up the cache. - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - - if (finalUpdateCacheResult) { - return null; - } else { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to update client side cache"); - } - } - - /** - * Clear custom installation id of the {@link FirebaseApp} on Firebase segmentation backend and - * client side cache. - * - *
-   *     The workflow is:
-   *                  get Firebase instance id and token
-   *                      |                      |
-   *                      |    update cache with cache status PENDING_CLEAR
-   *                      |                      |
-   *                    send http request to backend
-   *                                  |
-   *                   on success: delete cache entry
-   *                                  |
-   *                               return
-   * 
- */ - @WorkerThread - private Void clearCustomInstallationId() throws SetCustomInstallationIdException { - InstanceIdResult instanceIdResult; - try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); - } catch (ExecutionException | InterruptedException e) { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to get Firebase instance id"); - } - - boolean firstUpdateCacheResult = - localCache.insertOrUpdateCacheEntry( - CustomInstallationIdCacheEntryValue.create( - "", instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); - - if (!firstUpdateCacheResult) { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to update client side cache"); - } - - String iid = instanceIdResult.getId(); - String iidToken = instanceIdResult.getToken(); - Code backendRequestResult = - backendServiceClient.clearCustomInstallationId( - Utils.getProjectNumberFromAppId(firebaseApp.getOptions().getApplicationId()), - firebaseApp.getOptions().getApiKey(), - iid, - iidToken); - - boolean finalUpdateCacheResult; - switch (backendRequestResult) { - case OK: - finalUpdateCacheResult = localCache.clear(); - break; - case UNAUTHORIZED: - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Instance id token is invalid."); - case HTTP_CLIENT_ERROR: - throw new SetCustomInstallationIdException(Status.CLIENT_ERROR, "Http client error(4xx)"); - case NETWORK_ERROR: - case SERVER_ERROR: - default: - // These are considered retryable errors, so not to clean up the cache. - throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); - } - - if (finalUpdateCacheResult) { - return null; - } else { - throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to update client side cache"); - } - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java deleted file mode 100644 index ca7f688d60a..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrar.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.segmentation; - -import androidx.annotation.NonNull; -import com.google.firebase.FirebaseApp; -import com.google.firebase.components.Component; -import com.google.firebase.components.ComponentRegistrar; -import com.google.firebase.components.Dependency; -import com.google.firebase.platforminfo.LibraryVersionComponent; -import java.util.Arrays; -import java.util.List; - -public class FirebaseSegmentationRegistrar implements ComponentRegistrar { - - @Override - @NonNull - public List> getComponents() { - return Arrays.asList( - Component.builder(FirebaseSegmentation.class) - .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new FirebaseSegmentation(c.get(FirebaseApp.class))) - .build(), - LibraryVersionComponent.create("fire-segmentation", BuildConfig.VERSION_NAME)); - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java deleted file mode 100644 index 2291c078a03..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/SetCustomInstallationIdException.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.segmentation; - -import androidx.annotation.NonNull; -import com.google.firebase.FirebaseException; - -/** The class for all Exceptions thrown by {@link FirebaseSegmentation}. */ -public class SetCustomInstallationIdException extends FirebaseException { - - public enum Status { - UNKOWN(0), - - /** Error in Firebase SDK. */ - CLIENT_ERROR(1), - - /** Error when calling Firebase segmentation backend. */ - BACKEND_ERROR(2), - - /** The given custom installation is already tied to another Firebase installation. */ - DUPLICATED_CUSTOM_INSTALLATION_ID(3); - - private final int value; - - Status(int value) { - this.value = value; - } - } - - @NonNull private final Status status; - - SetCustomInstallationIdException(@NonNull Status status) { - this.status = status; - } - - SetCustomInstallationIdException(@NonNull Status status, @NonNull String message) { - super(message); - this.status = status; - } - - SetCustomInstallationIdException( - @NonNull Status status, @NonNull String message, Throwable cause) { - super(message, cause); - this.status = status; - } - - /** - * Gets the status code for the operation that failed. - * - * @return the code for the SetCustomInstallationIdException - */ - @NonNull - public Status getStatus() { - return status; - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java deleted file mode 100644 index ca231a89cb5..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.segmentation; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** Util methods used for {@link FirebaseSegmentation} */ -class Utils { - - private static final Pattern APP_ID_PATTERN = - Pattern.compile("^[^:]+:([0-9]+):(android|ios|web):([0-9a-f]+)"); - - static long getProjectNumberFromAppId(String appId) { - Matcher matcher = APP_ID_PATTERN.matcher(appId); - if (matcher.matches()) { - return Long.valueOf(matcher.group(1)); - } - throw new IllegalArgumentException("Invalid app id " + appId); - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java deleted file mode 100644 index 307d5d49923..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCache.java +++ /dev/null @@ -1,98 +0,0 @@ -// 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.local; - -import android.content.Context; -import android.content.SharedPreferences; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.firebase.FirebaseApp; - -/** - * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation - * backend API. - */ -public class CustomInstallationIdCache { - - // Status of each cache entry - // NOTE: never change the ordinal of the enum values because the enum values are stored in cache - // as their ordinal numbers. - public enum CacheStatus { - // Cache entry is synced to Firebase backend - SYNCED, - // Cache entry is waiting for Firebase backend response or internal network retry (for update - // operation). - PENDING_UPDATE, - // Cache entry is waiting for Firebase backend response or internal network retry (for clear - // operation). - PENDING_CLEAR - } - - private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; - - 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 final SharedPreferences prefs; - private final String persistenceKey; - - public CustomInstallationIdCache(@NonNull FirebaseApp firebaseApp) { - // Different FirebaseApp in the same Android application should have the same application - // context and same dir path - prefs = - firebaseApp - .getApplicationContext() - .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - persistenceKey = firebaseApp.getPersistenceKey(); - } - - @Nullable - public synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue() { - String cid = prefs.getString(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), null); - String iid = prefs.getString(getSharedPreferencesKey(INSTANCE_ID_KEY), null); - int status = prefs.getInt(getSharedPreferencesKey(CACHE_STATUS_KEY), -1); - - if (cid == null || iid == null || status == -1) { - return null; - } - - return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); - } - - @NonNull - public synchronized boolean insertOrUpdateCacheEntry( - @NonNull CustomInstallationIdCacheEntryValue entryValue) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString( - getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY), entryValue.getCustomInstallationId()); - editor.putString(getSharedPreferencesKey(INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); - editor.putInt(getSharedPreferencesKey(CACHE_STATUS_KEY), entryValue.getCacheStatus().ordinal()); - return editor.commit(); - } - - @NonNull - public synchronized boolean clear() { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(getSharedPreferencesKey(CUSTOM_INSTALLATION_ID_KEY)); - editor.remove(getSharedPreferencesKey(INSTANCE_ID_KEY)); - editor.remove(getSharedPreferencesKey(CACHE_STATUS_KEY)); - return editor.commit(); - } - - private String getSharedPreferencesKey(String key) { - return String.format("%s|%s", persistenceKey, key); - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java deleted file mode 100644 index 5e2c1944278..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheEntryValue.java +++ /dev/null @@ -1,44 +0,0 @@ -// 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.local; - -import androidx.annotation.NonNull; -import com.google.auto.value.AutoValue; -import com.google.firebase.segmentation.local.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 -public abstract class CustomInstallationIdCacheEntryValue { - @NonNull - public abstract String getCustomInstallationId(); - - @NonNull - public abstract String getFirebaseInstanceId(); - - @NonNull - public abstract CacheStatus getCacheStatus(); - - @NonNull - public static CustomInstallationIdCacheEntryValue create( - @NonNull String customInstallationId, - @NonNull String firebaseInstanceId, - @NonNull CacheStatus cacheStatus) { - return new AutoValue_CustomInstallationIdCacheEntryValue( - customInstallationId, firebaseInstanceId, cacheStatus); - } -} diff --git a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java deleted file mode 100644 index 65058bb97da..00000000000 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/remote/SegmentationServiceClient.java +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.segmentation.remote; - -import static com.google.firebase.segmentation.FirebaseSegmentation.TAG; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.util.Log; -import androidx.annotation.NonNull; -import com.google.android.gms.common.util.AndroidUtilsLight; -import com.google.android.gms.common.util.Hex; -import java.io.IOException; -import java.net.URL; -import java.util.zip.GZIPOutputStream; -import javax.net.ssl.HttpsURLConnection; -import org.json.JSONException; -import org.json.JSONObject; - -/** Http client that sends request to Firebase Segmentation backend API. To be implemented */ -public class SegmentationServiceClient { - - private static final String FIREBASE_SEGMENTATION_API_DOMAIN = - "firebasesegmentation.googleapis.com"; - private static final String UPDATE_REQUEST_RESOURCE_NAME_FORMAT = - "projects/%s/installations/%s/customSegmentationData"; - private static final String CLEAR_REQUEST_RESOURCE_NAME_FORMAT = - "projects/%s/installations/%s/customSegmentationData:clear"; - private static final String FIREBASE_SEGMENTATION_API_VERSION = "v1alpha"; - - private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type"; - private static final String JSON_CONTENT_TYPE = "application/json"; - private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; - private static final String GZIP_CONTENT_ENCODING = "gzip"; - private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; - private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; - - private final Context context; - - public SegmentationServiceClient(@NonNull Context context) { - this.context = context; - } - - public enum Code { - OK, - - CONFLICT, - - UNAUTHORIZED, - - NETWORK_ERROR, - - HTTP_CLIENT_ERROR, - - SERVER_ERROR, - } - - @NonNull - public Code updateCustomInstallationId( - long projectNumber, - @NonNull String apiKey, - @NonNull String customInstallationId, - @NonNull String firebaseInstanceId, - @NonNull String firebaseInstanceIdToken) { - String resourceName = - String.format(UPDATE_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); - try { - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_SEGMENTATION_API_DOMAIN, - FIREBASE_SEGMENTATION_API_VERSION, - resourceName, - apiKey)); - - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("PATCH"); - httpsURLConnection.addRequestProperty( - "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); - httpsURLConnection.addRequestProperty( - X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildUpdateCustomSegmentationDataRequestBody(resourceName, customInstallationId) - .toString() - .getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } - - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return Code.OK; - case 401: - return Code.UNAUTHORIZED; - case 409: - return Code.CONFLICT; - default: - if (httpResponseCode / 100 == 4) { - return Code.HTTP_CLIENT_ERROR; - } - return Code.SERVER_ERROR; - } - } catch (IOException e) { - return Code.NETWORK_ERROR; - } - } - - private static JSONObject buildUpdateCustomSegmentationDataRequestBody( - String resourceName, String customInstallationId) throws JSONException { - JSONObject customSegmentationData = new JSONObject(); - customSegmentationData.put("name", resourceName); - customSegmentationData.put("custom_installation_id", customInstallationId); - return customSegmentationData; - } - - @NonNull - public Code clearCustomInstallationId( - long projectNumber, - @NonNull String apiKey, - @NonNull String firebaseInstanceId, - @NonNull String firebaseInstanceIdToken) { - String resourceName = - String.format(CLEAR_REQUEST_RESOURCE_NAME_FORMAT, projectNumber, firebaseInstanceId); - try { - URL url = - new URL( - String.format( - "https://%s/%s/%s?key=%s", - FIREBASE_SEGMENTATION_API_DOMAIN, - FIREBASE_SEGMENTATION_API_VERSION, - resourceName, - apiKey)); - - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty( - "Authorization", "FIREBASE_INSTALLATIONS_AUTH " + firebaseInstanceIdToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); - httpsURLConnection.addRequestProperty( - X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildClearCustomSegmentationDataRequestBody(resourceName).toString().getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } - - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return Code.OK; - case 401: - return Code.UNAUTHORIZED; - default: - if (httpResponseCode / 100 == 4) { - return Code.HTTP_CLIENT_ERROR; - } - return Code.SERVER_ERROR; - } - } catch (IOException e) { - return Code.NETWORK_ERROR; - } - } - - private static JSONObject buildClearCustomSegmentationDataRequestBody(String resourceName) - throws JSONException { - return new JSONObject().put("name", resourceName); - } - - /** Gets the Android package's SHA-1 fingerprint. */ - private String getFingerprintHashForPackage() { - byte[] hash; - - try { - hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName()); - - if (hash == null) { - Log.e(TAG, "Could not get fingerprint hash for package: " + context.getPackageName()); - return null; - } else { - String cert = Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); - return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false); - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "No such package: " + context.getPackageName(), e); - return null; - } - } -} diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java deleted file mode 100644 index 56b0d120eb0..00000000000 --- a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationRegistrarTest.java +++ /dev/null @@ -1,54 +0,0 @@ -// 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 org.junit.Assert.assertNotNull; - -import androidx.test.core.app.ApplicationProvider; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseOptions; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class FirebaseSegmentationRegistrarTest { - - @Before - public void setUp() { - FirebaseApp.clearInstancesForTest(); - } - - @Test - public void getFirebaseInstallationsInstance() { - FirebaseApp defaultApp = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); - - FirebaseApp anotherApp = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), - "firebase_app_1"); - - FirebaseSegmentation defaultSegmentation = FirebaseSegmentation.getInstance(); - assertNotNull(defaultSegmentation); - - FirebaseSegmentation anotherSegmentation = FirebaseSegmentation.getInstance(anotherApp); - assertNotNull(anotherSegmentation); - } -} diff --git a/subprojects.cfg b/subprojects.cfg index d3070970235..e35f099a282 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -26,7 +26,6 @@ firebase-installations firebase-storage firebase-storage:ktx firebase-storage:test-app -firebase-segmentation protolite-well-known-types encoders From e54a54e784ed3281969132d9962c4ff0004faa93 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 9 Dec 2019 17:39:35 -0800 Subject: [PATCH 68/74] Add separator line back --- subprojects.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/subprojects.cfg b/subprojects.cfg index e35f099a282..33d16449cdc 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -26,6 +26,7 @@ firebase-installations firebase-storage firebase-storage:ktx firebase-storage:test-app + protolite-well-known-types encoders From 2840a510d28ae9d63dec749640eea455c1c4b486 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 10 Dec 2019 12:07:22 -0800 Subject: [PATCH 69/74] Update api info for FIS SDK --- firebase-installations/api.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index b4fd32edc8a..e2120cdf91f 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -1,15 +1,15 @@ // Signature format: 2.0 package com.google.firebase.installations { - public class FirebaseInstallations { - method @NonNull public Task delete(); - method @NonNull public Task getId(); + public class FirebaseInstallations implements com.google.firebase.installations.FirebaseInstallationsApi { + method @NonNull public com.google.android.gms.tasks.Task delete(); + method @NonNull public com.google.android.gms.tasks.Task getId(); method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(); - method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull FirebaseApp); - method @NonNull public Task getToken(int); + method @NonNull public static com.google.firebase.installations.FirebaseInstallations getInstance(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public com.google.android.gms.tasks.Task getToken(@com.google.firebase.installations.FirebaseInstallationsApi.AuthTokenOption int); } - public class FirebaseInstallationsException { + public class FirebaseInstallationsException extends com.google.firebase.FirebaseException { ctor public FirebaseInstallationsException(@NonNull com.google.firebase.installations.FirebaseInstallationsException.Status); ctor public FirebaseInstallationsException(@NonNull String, @NonNull com.google.firebase.installations.FirebaseInstallationsException.Status); ctor public FirebaseInstallationsException(@NonNull String, @NonNull com.google.firebase.installations.FirebaseInstallationsException.Status, @NonNull Throwable); @@ -35,7 +35,7 @@ package com.google.firebase.installations.local { } public class PersistedInstallation { - ctor public PersistedInstallation(@NonNull FirebaseApp); + ctor public PersistedInstallation(@NonNull com.google.firebase.FirebaseApp); method public void clearForTesting(); method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry insertOrUpdatePersistedInstallationEntry(@NonNull com.google.firebase.installations.local.PersistedInstallationEntry); method @NonNull public com.google.firebase.installations.local.PersistedInstallationEntry readPersistedInstallationEntryValue(); @@ -91,10 +91,10 @@ package com.google.firebase.installations.local { package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { - ctor public FirebaseInstallationServiceClient(@NonNull Context, @Nullable UserAgentPublisher, @Nullable HeartBeatInfo); - method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); - method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); - method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + ctor public FirebaseInstallationServiceClient(@NonNull android.content.Context, @Nullable com.google.firebase.platforminfo.UserAgentPublisher, @Nullable com.google.firebase.heartbeatinfo.HeartBeatInfo); + method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException; + method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.FirebaseException, java.io.IOException; + method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException; } public abstract class InstallationResponse { From 7e59264f2f3a8477462b38462fd767bc2f2d7d97 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Fri, 13 Dec 2019 16:02:59 -0800 Subject: [PATCH 70/74] changed the instrumentation tests into unit tests (#1053) * changed the instrumentation tests into unit tests unit tests are easier and faster to run and debug the unit tests are also included in the code coverage tests * remove the FirebaseInstallations instrumentation tests These have been moved into unit tests and so are no longer needed. Also removed constants that are no longer referenced. * fix a format error --- .../firebase-installations.gradle | 5 +- ...FirebaseInstallationsInstrumentedTest.java | 849 ---------------- .../FisAndroidTestConstants.java | 44 - .../PersistedInstallationEntrySubject.java | 2 +- .../installations/FirebaseInstallations.java | 4 +- .../firebase/installations/FakeCalendar.java | 2 + .../FirebaseInstallationsTest.java | 962 +++++++++++++++++- .../installations/TestOnCompleteListener.java | 70 ++ 8 files changed, 1039 insertions(+), 899 deletions(-) delete mode 100644 firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java rename firebase-installations/src/{androidTest => test}/java/com/google/firebase/installations/FakeCalendar.java (98%) create mode 100644 firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index 0ef070e8b6d..428083c4939 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -53,7 +53,8 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" - + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation 'org.mockito:mockito-inline:2.25.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' @@ -61,5 +62,5 @@ dependencies { androidTestImplementation 'junit:junit:4.12' androidTestImplementation "androidx.annotation:annotation:1.0.0" androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' + androidTestImplementation 'org.mockito:mockito-inline:2.25.0' } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java deleted file mode 100644 index 4fec8c4cce5..00000000000 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ /dev/null @@ -1,849 +0,0 @@ -// 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.installations; - -import static com.google.common.truth.Truth.assertWithMessage; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_API_KEY; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_APP_ID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_2; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_3; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_AUTH_TOKEN_4; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_CREATION_TIMESTAMP_2; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_FID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTALLATION_RESPONSE_WITH_IID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_INSTANCE_ID_1; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_PROJECT_ID; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_REFRESH_TOKEN; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_EXPIRATION_TIMESTAMP; -import static com.google.firebase.installations.FisAndroidTestConstants.TEST_TOKEN_RESULT; -import static com.google.firebase.installations.local.PersistedInstallationEntrySubject.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.matches; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.runner.AndroidJUnit4; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.FirebaseException; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.FirebaseInstallationsException.Status; -import com.google.firebase.installations.local.IidStore; -import com.google.firebase.installations.local.PersistedInstallation; -import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; -import com.google.firebase.installations.local.PersistedInstallationEntry; -import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; -import com.google.firebase.installations.remote.TokenResult; -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.MethodSorters; -import org.mockito.AdditionalAnswers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class FirebaseInstallationsInstrumentedTest { - private FirebaseApp firebaseApp; - private ExecutorService executor; - private PersistedInstallation persistedInstallation; - @Mock private FirebaseInstallationServiceClient mockBackend; - @Mock private IidStore mockIidStore; - @Mock private RandomFidGenerator mockFidGenerator; - - private static final PersistedInstallationEntry REGISTERED_INSTALLATION_ENTRY = - PersistedInstallationEntry.builder() - .setFirebaseInstallationId(TEST_FID_1) - .setAuthToken(TEST_AUTH_TOKEN) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setTokenCreationEpochInSecs(TEST_CREATION_TIMESTAMP_2) - .setExpiresInSecs(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setRegistrationStatus(PersistedInstallation.RegistrationStatus.REGISTERED) - .build(); - - private FirebaseInstallations firebaseInstallations; - private Utils utils; - private FakeCalendar fakeCalendar; - - @Before - public void setUp() throws FirebaseException, IOException { - MockitoAnnotations.initMocks(this); - FirebaseApp.clearInstancesForTest(); - executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); - fakeCalendar = new FakeCalendar(5000000L); - firebaseApp = - FirebaseApp.initializeApp( - ApplicationProvider.getApplicationContext(), - new FirebaseOptions.Builder() - .setApplicationId(TEST_APP_ID_1) - .setProjectId(TEST_PROJECT_ID) - .setApiKey(TEST_API_KEY) - .build()); - persistedInstallation = new PersistedInstallation(firebaseApp); - persistedInstallation.clearForTesting(); - - utils = new Utils(fakeCalendar); - firebaseInstallations = - new FirebaseInstallations( - executor, - firebaseApp, - mockBackend, - persistedInstallation, - utils, - mockIidStore, - mockFidGenerator); - - when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); - } - - @After - public void cleanUp() { - persistedInstallation.clearForTesting(); - try { - executor.awaitTermination(250, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - - } - } - - /** - * Check the id generation process when there is no network. There are three cases: - * - *
    - *
  • no iid -> generate a new fid - *
  • iid present -> make that iid into a fid - *
  • fid generated -> return that fid - *
- */ - @Test - public void testGetId_noNetwork_noIid() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockIidStore.readIid()).thenReturn(null); - - // Do the actual getId() call under test. Confirm that it returns a generated FID and - // and that the FID was written to storage. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_noNetwork_iidPresent() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - - // Do the actual getId() call under test. Confirm that it returns a generated FID and - // and that the FID was written to storage. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_INSTANCE_ID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); - - // Do the actual getId() call under test. Confirm that it returns the already generated FID. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo("generatedFid"); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid is registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid("generatedFid"); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - /** - * Checks that if we have a registered fid then the fid is returned and no backend calls are made. - */ - @Test - public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // No exception, means success. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second - // while we mock fid registration. We dont send an actual request to FIS in tests. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // check that the mockClient didn't get invoked at all, since the fid is already registered - // and the authtoken is present and not expired - verifyZeroInteractions(mockBackend); - - // check that the fid is still the expected one and is registered - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * Checks that if we have an unregistered fid that the fid gets registered with the backend and no - * other calls are made. - */ - @Test - public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { - when(mockBackend.createFirebaseInstallation( - anyString(), matches(TEST_FID_1), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // No exception, means success. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second - // while we mock fid registration. We dont send an actual request to FIS in tests. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // check that the mockClient didn't get invoked at all, since the fid is already registered - // and the authtoken is present and not expired - verify(mockBackend) - .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); - verify(mockBackend, never()) - .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - - // check that the fid is still the expected one and is registered - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - @Test - public void testGetId_migrateIid_successful() throws Exception { - when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); - - // Do the actual getId() call under test. - // Confirm both that it returns the expected ID, as does reading the prefs from storage. - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_INSTANCE_ID_1); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasFid(TEST_INSTANCE_ID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The storage should still have the same ID and the status should indicate that the - // fid si registered. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_INSTANCE_ID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - @Test - public void testGetId_multipleCalls_sameFIDReturned() throws Exception { - when(mockIidStore.readIid()).thenReturn(null); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - // Call getId multiple times - Task task1 = firebaseInstallations.getId(); - Task task2 = firebaseInstallations.getId(); - Tasks.await(Tasks.whenAllComplete(task1, task2)); - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - assertWithMessage("Persisted Fid of Task1 doesn't match.") - .that(task1.getResult()) - .isEqualTo(TEST_FID_1); - assertWithMessage("Persisted Fid of Task2 doesn't match.") - .that(task2.getResult()) - .isEqualTo(TEST_FID_1); - verify(mockBackend, times(1)) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * Checks that if the server rejects a FID during registration the SDK will use the fid in the - * response as the new fid. - */ - @Test - public void testGetId_unregistered_replacesFidWithResponse() throws Exception { - // Update local storage with installation entry that has invalid fid. - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - // The first call will return the existing FID, "tobereplaced" - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo("tobereplaced"); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // The next call should return the FID that was returned by the server - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - } - - /** - * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error - * state. - */ - @Test - public void testGetId_ServerError_UnregisteredFID() throws Exception { - // start with an unregistered fid - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // have the server return a server error for the registration - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); - - // do a getId(), the unregistered TEST_FID_1 should be returned - assertWithMessage("getId Task failed.") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers. - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // We expect that the server error will cause the FID to be put into the error state. - // There is nothing more we can do. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); - } - - /** - * A registration that fails with an IOException will not cause the FID to be put into the error - * state. - */ - @Test - public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { - // set initial state to having an unregistered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - // Mocking unchecked exception on FIS createFirebaseInstallation - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - String fid = Tasks.await(firebaseInstallations.getId()); - assertEquals("fid doesn't match expected", TEST_FID_1, fid); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // We expect that the IOException will cause the request to fail, but it will not - // cause the FID to be put into the error state because we expect this to eventually succeed. - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.UNREGISTERED); - } - - @Test - public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { - // Start with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Mocking unchecked exception on FIS generateAuthToken - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenThrow(new IOException()); - - assertWithMessage("getId Task failed") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that generates auth token with the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // Validate that registration status is still REGISTER - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasFid(TEST_FID_1); - assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTERED); - } - - /** - * The FID is successfully registered but the token is expired. A getId will cause the token to be - * refreshed in the background. - */ - @Test - public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { - // Start with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Get the ID, which should cause the SDK to realize that the auth token is expired and - // kick off a refresh of the token. - assertWithMessage("getId Task failed") - .that(Tasks.await(firebaseInstallations.getId())) - .isEqualTo(TEST_FID_1); - - // Waiting for Task that registers FID on the FIS Servers - executor.awaitTermination(500, TimeUnit.MILLISECONDS); - - // Check that the token has been refreshed - assertWithMessage("auth token is not what is expected after the refresh") - .that( - Tasks.await( - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)) - .getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - - verify(mockBackend, never()) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasAuthToken(TEST_AUTH_TOKEN); - } - - @Test - public void testGetAuthToken_fidExists_successful() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN); - verify(mockBackend, never()) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { - // start with a registered FID and valid auth token - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - // Move the time forward by the token expiration time. - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // have the server respond with a new token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - } - - @Test - public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { - // Update local storage with a unregistered installation entry to validate that getToken - // calls getId to ensure FID registration and returns a valid auth token. - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - InstallationTokenResult installationTokenResult = - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(installationTokenResult.getToken()) - .isEqualTo(TEST_AUTH_TOKEN); - } - - @Test - public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Mocks error during auth token generation - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail("the getAuthToken() call should have failed due to Auth Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(IOException.class); - } - - assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); - } - - // /** - // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call - // * fails. - // */ - @Test - public void testGetAuthToken_serverError_failure() throws Exception { - // start the test with a registered FID - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // have the backend fail when generateAuthToken is invoked. - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn( - TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); - - // Make the forced getAuthToken call, which should fail. - try { - Tasks.await(firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH)); - fail( - "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " - + "returned by the network call."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(Status.BAD_CONFIG); - } - } - - @Test - public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() - throws Exception { - // start with a valid fid and authtoken - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Make the server generateAuthToken() call return a refreshed token - when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_TOKEN_RESULT); - - // expire the authtoken by advancing the clock - fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); - - // Call getToken multiple times with DO_NOT_FORCE_REFRESH option - Task task1 = - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); - Task task2 = - firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); - - Tasks.await(Tasks.whenAllComplete(task1, task2)); - - assertWithMessage("Persisted Auth Token doesn't match") - .that(task1.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - assertWithMessage("Persisted Auth Token doesn't match") - .that(task2.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_2); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { - // start with a valid fid and authtoken - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withRegisteredFid( - TEST_FID_1, - TEST_REFRESH_TOKEN, - utils.currentTimeInSecs(), - TEST_AUTH_TOKEN, - TEST_TOKEN_EXPIRATION_TIMESTAMP)); - - // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not - // completed before the second task starts. Hence, we can test multiple calls to getToken() - // and verify one task waits for another task to complete. - - doAnswer( - AdditionalAnswers.answersWithDelay( - 500, - (unused) -> - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_3) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build())) - .doAnswer( - AdditionalAnswers.answersWithDelay( - 500, - (unused) -> - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_4) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build())) - .when(mockBackend) - .generateAuthToken(anyString(), anyString(), anyString(), anyString()); - - // Call getToken multiple times with FORCE_REFRESH option. - Task task1 = - firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); - Task task2 = - firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); - Tasks.await(Tasks.whenAllComplete(task1, task2)); - - // As we cannot ensure which task got executed first, verifying with both expected values - assertWithMessage("Persisted Auth Token doesn't match") - .that(task1.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_3); - assertWithMessage("Persisted Auth Token doesn't match") - .that(task2.getResult().getToken()) - .isEqualTo(TEST_AUTH_TOKEN_3); - verify(mockBackend, times(1)) - .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - PersistedInstallationEntry updatedInstallationEntry = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(updatedInstallationEntry).hasAuthToken(TEST_AUTH_TOKEN_3); - } - - @Test - public void testDelete_registeredFID_successful() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) - .thenReturn(TEST_INSTALLATION_RESPONSE); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); - verify(mockBackend, times(1)) - .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); - } - - @Test - public void testDelete_unregisteredFID_successful() throws Exception { - // Update local storage with a unregistered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); - verify(mockBackend, never()) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - } - - @Test - public void testDelete_emptyPersistedFidEntry_successful() throws Exception { - persistedInstallation.insertOrUpdatePersistedInstallationEntry( - PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); - - Tasks.await(firebaseInstallations.delete()); - - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).hasRegistrationStatus(RegistrationStatus.NOT_GENERATED); - verify(mockBackend, never()) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - } - - @Test - public void testDelete_serverError_badConfig() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - doThrow(new FirebaseException("Server Error")) - .when(mockBackend) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.delete()); - fail("firebaseInstallations.delete() failed due to Server Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(FirebaseInstallationsException.class); - assertWithMessage("Exception status doesn't match") - .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) - .isEqualTo(Status.BAD_CONFIG); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); - } - } - - @Test - public void testDelete_networkError() throws Exception { - // Update local storage with a registered installation entry - persistedInstallation.insertOrUpdatePersistedInstallationEntry(REGISTERED_INSTALLATION_ENTRY); - - doThrow(new IOException()) - .when(mockBackend) - .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); - - // Expect exception - try { - Tasks.await(firebaseInstallations.delete()); - fail("firebaseInstallations.delete() failed due to a Network Error."); - } catch (ExecutionException expected) { - assertWithMessage("Exception class doesn't match") - .that(expected) - .hasCauseThat() - .isInstanceOf(IOException.class); - PersistedInstallationEntry entryValue = - persistedInstallation.readPersistedInstallationEntryValue(); - assertThat(entryValue).isEqualTo(REGISTERED_INSTALLATION_ENTRY); - } - } -} diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java index 769ea8ec5a9..88b6dd62d4f 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FisAndroidTestConstants.java @@ -15,21 +15,11 @@ package com.google.firebase.installations; import com.google.firebase.installations.local.PersistedInstallationEntry; -import com.google.firebase.installations.remote.InstallationResponse; -import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; -import com.google.firebase.installations.remote.TokenResult; public final class FisAndroidTestConstants { public static final String TEST_FID_1 = "cccccccccccccccccccccc"; - public static final String TEST_PROJECT_ID = "777777777777"; - public static final String TEST_AUTH_TOKEN = "fis.auth.token"; - public static final String TEST_AUTH_TOKEN_2 = "fis.auth.token2"; - public static final String TEST_AUTH_TOKEN_3 = "fis.auth.token3"; - public static final String TEST_AUTH_TOKEN_4 = "fis.auth.token4"; - - public static final String TEST_API_KEY = "apiKey"; public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; @@ -41,40 +31,6 @@ public final class FisAndroidTestConstants { public static final long TEST_CREATION_TIMESTAMP_1 = 2000L; public static final long TEST_CREATION_TIMESTAMP_2 = 2L; - public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; - public static final PersistedInstallationEntry DEFAULT_PERSISTED_INSTALLATION_ENTRY = PersistedInstallationEntry.builder().build(); - public static final InstallationResponse TEST_INSTALLATION_RESPONSE = - InstallationResponse.builder() - .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) - .setFid(TEST_FID_1) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setAuthToken( - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()) - .setResponseCode(ResponseCode.OK) - .build(); - - public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = - InstallationResponse.builder() - .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) - .setFid(TEST_INSTANCE_ID_1) - .setRefreshToken(TEST_REFRESH_TOKEN) - .setAuthToken( - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .build()) - .setResponseCode(ResponseCode.OK) - .build(); - - public static final TokenResult TEST_TOKEN_RESULT = - TokenResult.builder() - .setToken(TEST_AUTH_TOKEN_2) - .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) - .setResponseCode(TokenResult.ResponseCode.OK) - .build(); } diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java index 702b4684d2a..d2b91e5a58d 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/local/PersistedInstallationEntrySubject.java @@ -45,7 +45,7 @@ public static PersistedInstallationEntrySubject assertThat( /** * Constructor for use by subclasses. If you want to create an instance of this class itself, call - * {@link Subject#check(String, PersistedInstallationEntry ..) check(...)}{@code .that(actual)}. + * {@link Subject#check(String, Object ...) check(...)}{@code .that(actual)}. * * @param metadata * @param actual diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 3dc316a7b8e..f6177fc4923 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -337,7 +337,7 @@ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { } /** Use file locking to acquire a lock that will also block other processes. */ - private FileLock getCrossProcessLock() { + FileLock getCrossProcessLock() { try { File file = new File(firebaseApp.getApplicationContext().getFilesDir(), LOCKFILE_NAME_GENERATE_FID); @@ -351,7 +351,7 @@ private FileLock getCrossProcessLock() { } /** Release a previously acquired lock. */ - private void releaseCrossProcessLock(FileLock fileLock) { + void releaseCrossProcessLock(FileLock fileLock) { try { fileLock.release(); } catch (IOException e) { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java b/firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java similarity index 98% rename from firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java rename to firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java index 3190d5911e8..536b479f876 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FakeCalendar.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FakeCalendar.java @@ -23,10 +23,12 @@ public FakeCalendar(long initialTimeInMillis) { timeInMillis = initialTimeInMillis; } + @Override public long getTimeInMillis() { return timeInMillis; } + @Override public void setTimeInMillis(long timeInMillis) { this.timeInMillis = timeInMillis; } diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java index fe64dcf06b8..5dc10b77fda 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -14,5 +14,965 @@ package com.google.firebase.installations; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.tasks.Task; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.installations.FirebaseInstallationsException.Status; +import com.google.firebase.installations.local.IidStore; +import com.google.firebase.installations.local.PersistedInstallation; +import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; +import com.google.firebase.installations.local.PersistedInstallationEntry; +import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; +import com.google.firebase.installations.remote.InstallationResponse; +import com.google.firebase.installations.remote.InstallationResponse.ResponseCode; +import com.google.firebase.installations.remote.TokenResult; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + /** Tests for {@link FirebaseInstallations}. */ -public class FirebaseInstallationsTest {} +@RunWith(RobolectricTestRunner.class) +public class FirebaseInstallationsTest { + private FirebaseApp firebaseApp; + private ExecutorService executor; + private PersistedInstallation persistedInstallation; + @Mock private FirebaseInstallationServiceClient mockBackend; + @Mock private IidStore mockIidStore; + @Mock private RandomFidGenerator mockFidGenerator; + + public static final String TEST_FID_1 = "cccccccccccccccccccccc"; + + public static final String TEST_PROJECT_ID = "777777777777"; + + public static final String TEST_AUTH_TOKEN = "fis.auth.token"; + public static final String TEST_AUTH_TOKEN_2 = "fis.auth.token2"; + public static final String TEST_AUTH_TOKEN_3 = "fis.auth.token3"; + public static final String TEST_AUTH_TOKEN_4 = "fis.auth.token4"; + + public static final String TEST_API_KEY = "apiKey"; + + public static final String TEST_REFRESH_TOKEN = "1:test-refresh-token"; + + public static final String TEST_APP_ID_1 = "1:123456789:android:abcdef"; + + public static final long TEST_TOKEN_EXPIRATION_TIMESTAMP = 4000L; + + public static final String TEST_INSTANCE_ID_1 = "ccccccccccc"; + + public static final InstallationResponse TEST_INSTALLATION_RESPONSE = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_FID_1) + .setFid(TEST_FID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + + public static final InstallationResponse TEST_INSTALLATION_RESPONSE_WITH_IID = + InstallationResponse.builder() + .setUri("/projects/" + TEST_PROJECT_ID + "/installations/" + TEST_INSTANCE_ID_1) + .setFid(TEST_INSTANCE_ID_1) + .setRefreshToken(TEST_REFRESH_TOKEN) + .setAuthToken( + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .build()) + .setResponseCode(ResponseCode.OK) + .build(); + + public static final TokenResult TEST_TOKEN_RESULT = + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_2) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build(); + + private FirebaseInstallations firebaseInstallations; + private Utils utils; + private FakeCalendar fakeCalendar; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FirebaseApp.clearInstancesForTest(); + executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + fakeCalendar = new FakeCalendar(5000000L); + firebaseApp = + FirebaseApp.initializeApp( + ApplicationProvider.getApplicationContext(), + new FirebaseOptions.Builder() + .setApplicationId(TEST_APP_ID_1) + .setProjectId(TEST_PROJECT_ID) + .setApiKey(TEST_API_KEY) + .build()); + persistedInstallation = new PersistedInstallation(firebaseApp); + persistedInstallation.clearForTesting(); + + utils = new Utils(fakeCalendar); + firebaseInstallations = + new FirebaseInstallations( + executor, + firebaseApp, + mockBackend, + persistedInstallation, + utils, + mockIidStore, + mockFidGenerator); + + when(mockFidGenerator.createRandomFid()).thenReturn(TEST_FID_1); + } + + @After + public void cleanUp() { + persistedInstallation.clearForTesting(); + try { + executor.awaitTermination(250, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + + } + } + + /** + * Check the id generation process when there is no network. There are three cases: + * + *
    + *
  • no iid -> generate a new fid + *
  • iid present -> make that iid into a fid + *
  • fid generated -> return that fid + *
+ */ + @Test + public void testGetId_noNetwork_noIid() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(null); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry isn't unregistered: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_noNetwork_iidPresent() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + + // Do the actual getId() call under test. Confirm that it returns a generated FID and + // and that the FID was written to storage. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + assertTrue("the entry doesn't have an uregistered fid: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("generatedFid")); + + // Do the actual getId() call under test. Confirm that it returns the already generated FID. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo("generatedFid"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid is registered. + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo("generatedFid")); + assertTrue("the entry doesn't have an uregistered fid: " + entry, entry.isUnregistered()); + } + + /** + * Checks that if we have a registered fid then the fid is returned and no backend calls are made. + */ + @Test + public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // No exception, means success. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verifyZeroInteractions(mockBackend); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * Checks that if we have an unregistered fid that the fid gets registered with the backend and no + * other calls are made. + */ + @Test + public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { + when(mockBackend.createFirebaseInstallation( + anyString(), matches(TEST_FID_1), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // No exception, means success. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // getId() returns fid immediately but registers fid asynchronously. Waiting for half a second + // while we mock fid registration. We dont send an actual request to FIS in tests. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // check that the mockClient didn't get invoked at all, since the fid is already registered + // and the authtoken is present and not expired + verify(mockBackend) + .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); + verify(mockBackend, never()) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // check that the fid is still the expected one and is registered + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + @Test + public void testGetId_migrateIid_successful() throws Exception { + when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); + + // Do the actual getId() call under test. + // Confirm both that it returns the expected ID, as does reading the prefs from storage. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_INSTANCE_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The storage should still have the same ID and the status should indicate that the + // fid si registered. + entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_INSTANCE_ID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + @Test + public void testGetId_multipleCalls_sameFIDReturned() throws Exception { + when(mockIidStore.readIid()).thenReturn(null); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + // Call getId multiple times + Task task1 = firebaseInstallations.getId(); + Task task2 = firebaseInstallations.getId(); + TestOnCompleteListener onCompleteListener1 = new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + assertWithMessage("Persisted Fid of Task1 doesn't match.") + .that(task1.getResult()) + .isEqualTo(TEST_FID_1); + assertWithMessage("Persisted Fid of Task2 doesn't match.") + .that(task2.getResult()) + .isEqualTo(TEST_FID_1); + verify(mockBackend, times(1)) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry isn't doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * Checks that if the server rejects a FID during registration the SDK will use the fid in the + * response as the new fid. + */ + @Test + public void testGetId_unregistered_replacesFidWithResponse() throws Exception { + // Update local storage with installation entry that has invalid fid. + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + // The first call will return the existing FID, "tobereplaced" + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo("tobereplaced"); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // The next call should return the FID that was returned by the server + onCompleteListener = new TestOnCompleteListener<>(); + task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + } + + /** + * A registration that fails with a SERVER_ERROR will cause the FID to be put into the error + * state. + */ + @Test + public void testGetId_ServerError_UnregisteredFID() throws Exception { + // start with an unregistered fid + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // have the server return a server error for the registration + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task task = firebaseInstallations.getId(); + task.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + // do a getId(), the unregistered TEST_FID_1 should be returned + assertWithMessage("getId Task failed.").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers. + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // We expect that the server error will cause the FID to be put into the error state. + // There is nothing more we can do. + PersistedInstallationEntry updatedInstallationEntry = + persistedInstallation.readPersistedInstallationEntryValue(); + // //assertThat(TEST_FID_1, eq(updatedInstallationEntry.getFirebaseInstallationId())); + // //assertThat(updatedInstallationEntry).hasRegistrationStatus(RegistrationStatus.REGISTER_ERROR); + } + + /** + * A registration that fails with an IOException will not cause the FID to be put into the error + * state. + */ + @Test + public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws Exception { + // set initial state to having an unregistered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + // Mocking unchecked exception on FIS createFirebaseInstallation + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + assertEquals("fid doesn't match expected", TEST_FID_1, fid); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // We expect that the IOException will cause the request to fail, but it will not + // cause the FID to be put into the error state because we expect this to eventually succeed. + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have an unregistered fid: " + entry, entry.isUnregistered()); + } + + @Test + public void testGetId_expiredAuthTokenUncheckedException_statusUpdated() throws Exception { + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Mocking unchecked exception on FIS generateAuthToken + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenThrow(new IOException()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + + assertWithMessage("getId Task failed").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that generates auth token with the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + // Validate that registration status is still REGISTER + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); + assertTrue("the entry doesn't have a registered fid: " + entry, entry.isRegistered()); + } + + /** + * The FID is successfully registered but the token is expired. A getId will cause the token to be + * refreshed in the background. + */ + @Test + public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { + // Start with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Get the ID, which should cause the SDK to realize that the auth token is expired and + // kick off a refresh of the token. + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + Task getIdTask = firebaseInstallations.getId(); + getIdTask.addOnCompleteListener(executor, onCompleteListener); + String fid = onCompleteListener.await(); + assertWithMessage("getId Task failed").that(fid).isEqualTo(TEST_FID_1); + + // Waiting for Task that registers FID on the FIS Servers + executor.awaitTermination(500, TimeUnit.MILLISECONDS); + + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener2); + InstallationTokenResult installationTokenResult = onCompleteListener2.await(); + + // Check that the token has been refreshed + assertWithMessage("auth token is not what is expected after the refresh") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + + verify(mockBackend, never()) + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getAuthToken(), equalTo(TEST_AUTH_TOKEN)); + } + + @Test + public void testGetAuthToken_fidExists_successful() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + verify(mockBackend, never()) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testGetAuthToken_expiredAuthToken_fetchedNewTokenFromFIS() throws Exception { + // start with a registered FID and valid auth token + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Move the time forward by the token expiration time. + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // have the server respond with a new token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + } + + @Test + public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Exception { + // Update local storage with a unregistered installation entry to validate that getToken + // calls getId to ensure FID registration and returns a valid auth token. + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + InstallationTokenResult installationTokenResult = onCompleteListener.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(installationTokenResult.getToken()) + .isEqualTo(TEST_AUTH_TOKEN); + } + + @Test + public void testGetAuthToken_authError_persistedInstallationCleared() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Mocks error during auth token generation + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.AUTH_ERROR).build()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("the getAuthToken() call should have failed due to Auth Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); + } + + assertTrue(persistedInstallation.readPersistedInstallationEntryValue().isNotGenerated()); + } + + // /** + // * Check that a call to generateAuthToken(FORCE_REFRESH) fails if the backend client call + // * fails. + // */ + @Test + public void testGetAuthToken_serverError_failure() throws Exception { + // start the test with a registered FID + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // have the backend fail when generateAuthToken is invoked. + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn( + TokenResult.builder().setResponseCode(TokenResult.ResponseCode.BAD_CONFIG).build()); + + // Make the forced getAuthToken call, which should fail. + try { + TestOnCompleteListener onCompleteListener = + new TestOnCompleteListener<>(); + Task task = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + task.addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail( + "getAuthToken() succeeded but should have failed due to the BAD_CONFIG error " + + "returned by the network call."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(Status.BAD_CONFIG); + } + } + + @Test + public void testGetAuthToken_multipleCallsDoNotForceRefresh_fetchedNewTokenOnce() + throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Make the server generateAuthToken() call return a refreshed token + when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_TOKEN_RESULT); + + // expire the authtoken by advancing the clock + fakeCalendar.advanceTimeBySeconds(TEST_TOKEN_EXPIRATION_TIMESTAMP); + + // Call getToken multiple times with DO_NOT_FORCE_REFRESH option + Task task1 = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + Task task2 = + firebaseInstallations.getToken(FirebaseInstallationsApi.DO_NOT_FORCE_REFRESH); + TestOnCompleteListener onCompleteListener1 = + new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_2); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + @Ignore("the code doesn't currently enforce a single token fetch at a time") + public void testGetAuthToken_multipleCallsForceRefresh_fetchedNewTokenTwice() throws Exception { + // start with a valid fid and authtoken + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + // Use a mock ServiceClient for network calls with delay(500ms) to ensure first task is not + // completed before the second task starts. Hence, we can test multiple calls to getToken() + // and verify one task waits for another task to complete. + + doAnswer( + AdditionalAnswers.answersWithDelay( + 500, + (unused) -> + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_3) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build())) + .doAnswer( + AdditionalAnswers.answersWithDelay( + 500, + (unused) -> + TokenResult.builder() + .setToken(TEST_AUTH_TOKEN_4) + .setTokenExpirationTimestamp(TEST_TOKEN_EXPIRATION_TIMESTAMP) + .setResponseCode(TokenResult.ResponseCode.OK) + .build())) + .when(mockBackend) + .generateAuthToken(anyString(), anyString(), anyString(), anyString()); + + // Call getToken multiple times with FORCE_REFRESH option. + // Call getToken multiple times with DO_NOT_FORCE_REFRESH option + Task task1 = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + Task task2 = + firebaseInstallations.getToken(FirebaseInstallationsApi.FORCE_REFRESH); + TestOnCompleteListener onCompleteListener1 = + new TestOnCompleteListener<>(); + task1.addOnCompleteListener(executor, onCompleteListener1); + TestOnCompleteListener onCompleteListener2 = + new TestOnCompleteListener<>(); + task2.addOnCompleteListener(executor, onCompleteListener2); + onCompleteListener1.await(); + onCompleteListener2.await(); + + // As we cannot ensure which task got executed first, verifying with both expected values + assertWithMessage("Persisted Auth Token doesn't match") + .that(task1.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_3); + assertWithMessage("Persisted Auth Token doesn't match") + .that(task2.getResult().getToken()) + .isEqualTo(TEST_AUTH_TOKEN_3); + verify(mockBackend, times(1)) + .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertThat(entry.getAuthToken(), equalTo(TEST_AUTH_TOKEN_3)); + } + + @Test + public void testDelete_registeredFID_successful() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + .thenReturn(TEST_INSTALLATION_RESPONSE); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, times(1)) + .deleteFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); + } + + @Test + public void testDelete_unregisteredFID_successful() throws Exception { + // Update local storage with a unregistered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entryValue = + persistedInstallation.readPersistedInstallationEntryValue(); + assertEquals(entryValue.getRegistrationStatus(), RegistrationStatus.NOT_GENERATED); + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + } + + @Test + public void testDelete_emptyPersistedFidEntry_successful() throws Exception { + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withNoGeneratedFid()); + + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + + PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue( + "the entry was expected to need a newly generated fid: " + entry, entry.isNotGenerated()); + + verify(mockBackend, never()) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + } + + @Test + public void testDelete_serverError_badConfig() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + doThrow(new FirebaseException("Server Error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("firebaseInstallations.delete() failed due to Server Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(FirebaseInstallationsException.class); + assertWithMessage("Exception status doesn't match") + .that(((FirebaseInstallationsException) expected.getCause()).getStatus()) + .isEqualTo(Status.BAD_CONFIG); + PersistedInstallationEntry entry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue("the entry was expected to still be registered: " + entry, entry.isRegistered()); + } + } + + @Test + public void testDelete_networkError() throws Exception { + // Update local storage with a registered installation entry + persistedInstallation.insertOrUpdatePersistedInstallationEntry( + PersistedInstallationEntry.INSTANCE.withRegisteredFid( + TEST_FID_1, + TEST_REFRESH_TOKEN, + utils.currentTimeInSecs(), + TEST_AUTH_TOKEN, + TEST_TOKEN_EXPIRATION_TIMESTAMP)); + + doThrow(new IOException("simulated network error")) + .when(mockBackend) + .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); + + // Expect exception + try { + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseInstallations.delete().addOnCompleteListener(executor, onCompleteListener); + onCompleteListener.await(); + fail("firebaseInstallations.delete() should have failed due to a Network Error."); + } catch (ExecutionException expected) { + assertWithMessage("Exception class doesn't match") + .that(expected) + .hasCauseThat() + .isInstanceOf(IOException.class); + PersistedInstallationEntry entry = + persistedInstallation.readPersistedInstallationEntryValue(); + assertTrue( + "the entry was expected to still be registered since the delete failed: " + entry, + entry.isRegistered()); + } + } +} diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java b/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java new file mode 100644 index 00000000000..600c463e42b --- /dev/null +++ b/firebase-installations/src/test/java/com/google/firebase/installations/TestOnCompleteListener.java @@ -0,0 +1,70 @@ +// 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.installations; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Helper listener that works around a limitation of the Tasks API where await() cannot be called on + * the main thread. This listener works around it by running itself on a different thread, thus + * allowing the main thread to be woken up when the Tasks complete. + */ +public class TestOnCompleteListener implements OnCompleteListener { + private static final long TIMEOUT_MS = 5000; + private final CountDownLatch latch = new CountDownLatch(1); + private Task task; + private volatile TResult result; + private volatile Exception exception; + private volatile boolean successful; + + @Override + public void onComplete(@NonNull Task task) { + this.task = task; + successful = task.isSuccessful(); + if (successful) { + result = task.getResult(); + } else { + exception = task.getException(); + } + latch.countDown(); + } + + /** Blocks until the {@link #onComplete} is called. */ + public TResult await() throws InterruptedException, ExecutionException { + if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new InterruptedException("timed out waiting for result"); + } + if (successful) { + return result; + } else { + if (exception instanceof InterruptedException) { + throw (InterruptedException) exception; + } + if (exception instanceof FirebaseInstallationsException) { + throw new ExecutionException(exception); + } + if (exception instanceof IOException) { + throw new ExecutionException(exception); + } + throw new IllegalStateException("got an unexpected exception type", exception); + } + } +} From e3375f245c56f1f30e1364303e74c3974ad61710 Mon Sep 17 00:00:00 2001 From: Fred Quintana Date: Mon, 6 Jan 2020 14:04:13 -0800 Subject: [PATCH 71/74] fixed a leak when cross process locking didn't close a file An intermediate RandomAccessFile created while acquiring the lock was not closed. Also pulled the cross process locking logic out into a separate class. --- .../installations/CrossProcessLock.java | 56 +++++++++++++++++++ .../installations/FirebaseInstallations.java | 32 +---------- 2 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 firebase-installations/src/main/java/com/google/firebase/installations/CrossProcessLock.java diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/CrossProcessLock.java b/firebase-installations/src/main/java/com/google/firebase/installations/CrossProcessLock.java new file mode 100644 index 00000000000..08594487ddb --- /dev/null +++ b/firebase-installations/src/main/java/com/google/firebase/installations/CrossProcessLock.java @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.installations; + +import android.content.Context; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; + +/** Use file locking to acquire a lock that will also block other processes. */ +class CrossProcessLock { + private final FileChannel channel; + private final FileLock lock; + + private CrossProcessLock(FileChannel channel, FileLock lock) { + this.channel = channel; + this.lock = lock; + } + + static CrossProcessLock acquire(Context appContext, String lockName) { + try { + File file = new File(appContext.getFilesDir(), lockName); + FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); + // Use the file channel to create a lock on the file. + // This method blocks until it can retrieve the lock. + FileLock lock = channel.lock(); + return new CrossProcessLock(channel, lock); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); + } + } + + /** Release a previously acquired lock and free any underlying resources. */ + void releaseAndClose() { + try { + lock.release(); + channel.close(); + } catch (IOException e) { + throw new IllegalStateException("exception while using file locks, should never happen", e); + } + } +} diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index f6177fc4923..7abd5351ed7 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -33,11 +33,7 @@ import com.google.firebase.installations.remote.InstallationResponse; import com.google.firebase.installations.remote.TokenResult; import com.google.firebase.platforminfo.UserAgentPublisher; -import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.util.ArrayList; import java.util.Calendar; import java.util.Iterator; @@ -311,7 +307,8 @@ private final void doRegistrationInternal(boolean forceRefresh) { * been persisted. */ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { - FileLock fileLock = getCrossProcessLock(); + CrossProcessLock lock = CrossProcessLock + .acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID); try { synchronized (lockGenerateFid) { PersistedInstallationEntry prefs = @@ -332,30 +329,7 @@ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { } } finally { - releaseCrossProcessLock(fileLock); - } - } - - /** Use file locking to acquire a lock that will also block other processes. */ - FileLock getCrossProcessLock() { - try { - File file = - new File(firebaseApp.getApplicationContext().getFilesDir(), LOCKFILE_NAME_GENERATE_FID); - FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); - // Use the file channel to create a lock on the file. - // This method blocks until it can retrieve the lock. - return channel.lock(); - } catch (IOException e) { - throw new IllegalStateException("exception while using file locks, should never happen", e); - } - } - - /** Release a previously acquired lock. */ - void releaseCrossProcessLock(FileLock fileLock) { - try { - fileLock.release(); - } catch (IOException e) { - throw new IllegalStateException("exception while using file locks, should never happen", e); + lock.releaseAndClose(); } } From ef08161e103ddd55e22630d2023c18b0c019a40c Mon Sep 17 00:00:00 2001 From: Ankita Jhawar Date: Tue, 7 Jan 2020 10:20:02 -0800 Subject: [PATCH 72/74] Reading the iid token from iid shared prefs. --- firebase-installations/api.txt | 1 + .../installations/local/IidStore.java | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index e2120cdf91f..4e5eb23d352 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -32,6 +32,7 @@ package com.google.firebase.installations.local { public class IidStore { ctor public IidStore(); method @Nullable public String readIid(); + method @Nullable public String readToken(); } public class PersistedInstallation { diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java index 4e8366df7bf..e572f734a79 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java @@ -30,6 +30,8 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; +import org.json.JSONException; +import org.json.JSONObject; /** * Read existing iid only for default (first initialized) instance of this firebase application.* @@ -38,10 +40,16 @@ public class IidStore { private static final String IID_SHARED_PREFS_NAME = "com.google.android.gms.appid"; private static final String STORE_KEY_PUB = "|S||P|"; private static final String STORE_KEY_ID = "|S|id"; + private static final String STORE_KEY_TOKEN = "|T|"; + private static final String DEFAULT_SCOPE = "|*"; + private static final String JSON_TOKEN_KEY = "token"; + private static final String JSON_ENCODED_PREFIX = "{"; @GuardedBy("iidPrefs") private final SharedPreferences iidPrefs; + private final String defaultSenderId; + public IidStore() { // Different FirebaseApp in the same Android application should have the same application // context and same dir path. We only read existing Iids for the default firebase application. @@ -49,6 +57,62 @@ public IidStore() { FirebaseApp.getInstance() .getApplicationContext() .getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE); + + defaultSenderId = getDefaultSenderId(FirebaseApp.getInstance()); + } + + private static String getDefaultSenderId(FirebaseApp app) { + // Check for an explicit sender id + String senderId = app.getOptions().getGcmSenderId(); + if (senderId != null) { + return senderId; + } + String appId = app.getOptions().getApplicationId(); + if (!appId.startsWith("1:")) { + // Not v1, server should be updated to accept the full app ID now + return appId; + } + // For v1 app IDs, fall back to parsing the project number out + @SuppressWarnings("StringSplitter") + String[] parts = appId.split(":"); + if (parts.length < 2) { + return null; // Invalid format + } + String projectNumber = parts[1]; + if (projectNumber.isEmpty()) { + return null; // No project number + } + return projectNumber; + } + + private String createTokenKey(@NonNull String senderId) { + return STORE_KEY_TOKEN + senderId + DEFAULT_SCOPE; + } + + @Nullable + public String readToken() { + synchronized (iidPrefs) { + String token = iidPrefs.getString(createTokenKey(defaultSenderId), null); + if (token.isEmpty()) { + return null; + } + + if (token.startsWith(JSON_ENCODED_PREFIX)) { + return parseIidTokenFromJson(token); + } + // Legacy value, token is whole string + return token; + } + } + + private String parseIidTokenFromJson(String token) { + // Encoded as JSON + try { + JSONObject json = new JSONObject(token); + return json.getString(JSON_TOKEN_KEY); + } catch (JSONException e) { + return null; + } } @Nullable From e69bd4d63046256672b095fac4a6513f25f4db47 Mon Sep 17 00:00:00 2001 From: Ankita Date: Tue, 7 Jan 2020 15:12:44 -0800 Subject: [PATCH 73/74] Adding iidMigrationToken in the create installation request header. (#1096) --- firebase-installations/api.txt | 2 +- .../installations/FirebaseInstallations.java | 7 +-- .../FirebaseInstallationServiceClient.java | 19 ++++++-- .../FirebaseInstallationsTest.java | 43 ++++++++++++------- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index 4e5eb23d352..2024c519fd0 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -93,7 +93,7 @@ package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { ctor public FirebaseInstallationServiceClient(@NonNull android.content.Context, @Nullable com.google.firebase.platforminfo.UserAgentPublisher, @Nullable com.google.firebase.heartbeatinfo.HeartBeatInfo); - method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException; + method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @Nullable String) throws java.io.IOException; method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.FirebaseException, java.io.IOException; method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException; } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 7abd5351ed7..8288d2c205f 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -307,8 +307,8 @@ private final void doRegistrationInternal(boolean forceRefresh) { * been persisted. */ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { - CrossProcessLock lock = CrossProcessLock - .acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID); + CrossProcessLock lock = + CrossProcessLock.acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID); try { synchronized (lockGenerateFid) { PersistedInstallationEntry prefs = @@ -354,7 +354,8 @@ private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEn /*apiKey= */ firebaseApp.getOptions().getApiKey(), /*fid= */ prefs.getFirebaseInstallationId(), /*projectID= */ firebaseApp.getOptions().getProjectId(), - /*appId= */ getApplicationId()); + /*appId= */ getApplicationId(), + /* migration-header= */ null); switch (response.getResponseCode()) { case OK: diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index bb392f9472e..a92dd0ba922 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -70,6 +70,8 @@ public class FirebaseInstallationServiceClient { private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; + private static final String X_ANDROID_IID_MIGRATION_KEY = "x-goog-fis-android-iid-migration-auth"; + private static final int NETWORK_TIMEOUT_MILLIS = 10000; private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s"); @@ -87,10 +89,10 @@ public class FirebaseInstallationServiceClient { public FirebaseInstallationServiceClient( @NonNull Context context, @Nullable UserAgentPublisher publisher, - @Nullable HeartBeatInfo heartBeatInfo) { + @Nullable HeartBeatInfo heartbeatInfo) { this.context = context; this.userAgentPublisher = publisher; - this.heartbeatInfo = heartBeatInfo; + this.heartbeatInfo = heartbeatInfo; } /** @@ -100,6 +102,8 @@ public FirebaseInstallationServiceClient( * @param fid Firebase Installation Identifier * @param projectID Project Id * @param appId the identifier of a Firebase application + * @param iidToken the identifier token of a Firebase application with instance id. It is set to + * null for a FID. * @return {@link InstallationResponse} generated from the response body *
    *
  • 400: return response with status BAD_CONFIG @@ -111,7 +115,11 @@ public FirebaseInstallationServiceClient( */ @NonNull public InstallationResponse createFirebaseInstallation( - @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) + @NonNull String apiKey, + @NonNull String fid, + @NonNull String projectID, + @NonNull String appId, + @Nullable String iidToken) throws IOException { String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); int retryCount = 0; @@ -128,6 +136,11 @@ public InstallationResponse createFirebaseInstallation( httpsURLConnection.setRequestMethod("POST"); httpsURLConnection.setDoOutput(true); + // Note: Set the iid token header for authenticating the Instance-ID migrating to FIS. + if (iidToken != null) { + httpsURLConnection.addRequestProperty(X_ANDROID_IID_MIGRATION_KEY, iidToken); + } + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(httpsURLConnection.getOutputStream()); try { diff --git a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java index 5dc10b77fda..9cabfa99494 100644 --- a/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java +++ b/firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doAnswer; @@ -178,7 +179,8 @@ public void cleanUp() { */ @Test public void testGetId_noNetwork_noIid() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenThrow(new IOException()); when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); @@ -207,7 +209,8 @@ public void testGetId_noNetwork_noIid() throws Exception { @Test public void testGetId_noNetwork_iidPresent() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenThrow(new IOException()); when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); @@ -236,7 +239,8 @@ public void testGetId_noNetwork_iidPresent() throws Exception { @Test public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenThrow(new IOException()); when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString())) .thenThrow(new IOException()); @@ -303,7 +307,7 @@ public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception { @Test public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { when(mockBackend.createFirebaseInstallation( - anyString(), matches(TEST_FID_1), anyString(), anyString())) + anyString(), matches(TEST_FID_1), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); @@ -322,7 +326,8 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { // check that the mockClient didn't get invoked at all, since the fid is already registered // and the authtoken is present and not expired verify(mockBackend) - .createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString()); + .createFirebaseInstallation( + anyString(), matches(TEST_FID_1), anyString(), anyString(), any()); verify(mockBackend, never()) .generateAuthToken(anyString(), anyString(), anyString(), anyString()); @@ -335,7 +340,8 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception { @Test public void testGetId_migrateIid_successful() throws Exception { when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID); // Do the actual getId() call under test. @@ -361,7 +367,8 @@ public void testGetId_migrateIid_successful() throws Exception { @Test public void testGetId_multipleCalls_sameFIDReturned() throws Exception { when(mockIidStore.readIid()).thenReturn(null); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); // Call getId multiple times @@ -384,7 +391,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception { .that(task2.getResult()) .isEqualTo(TEST_FID_1); verify(mockBackend, times(1)) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null); PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue(); assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1)); assertTrue("the entry isn't doesn't have a registered fid: " + entry, entry.isRegistered()); @@ -399,7 +406,8 @@ public void testGetId_unregistered_replacesFidWithResponse() throws Exception { // Update local storage with installation entry that has invalid fid. persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced")); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); // The first call will return the existing FID, "tobereplaced" @@ -435,7 +443,8 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception { PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); // have the server return a server error for the registration - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn( InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build()); @@ -469,7 +478,8 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); // Mocking unchecked exception on FIS createFirebaseInstallation - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenThrow(new IOException()); TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); @@ -569,14 +579,15 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception { .isEqualTo(TEST_AUTH_TOKEN_2); verify(mockBackend, never()) - .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1); + .createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null); verify(mockBackend, times(1)) .generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN); } @Test public void testGetAuthToken_fidDoesNotExist_successful() throws Exception { - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); TestOnCompleteListener onCompleteListener = @@ -650,7 +661,8 @@ public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Excepti // calls getId to ensure FID registration and returns a valid auth token. persistedInstallation.insertOrUpdatePersistedInstallationEntry( PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1)); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); TestOnCompleteListener onCompleteListener = @@ -859,7 +871,8 @@ public void testDelete_registeredFID_successful() throws Exception { utils.currentTimeInSecs(), TEST_AUTH_TOKEN, TEST_TOKEN_EXPIRATION_TIMESTAMP)); - when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString())) + when(mockBackend.createFirebaseInstallation( + anyString(), anyString(), anyString(), anyString(), any())) .thenReturn(TEST_INSTALLATION_RESPONSE); TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); From 84e1716e9fa5efbf3e6eb2b828ca045ca50de1fc Mon Sep 17 00:00:00 2001 From: Ankita Jhawar Date: Tue, 7 Jan 2020 15:13:28 -0800 Subject: [PATCH 74/74] Addressing Rayo's comments --- .../firebase/installations/FirebaseInstallations.java | 4 ++-- .../com/google/firebase/installations/local/IidStore.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 7abd5351ed7..67bc8db6fcb 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -307,8 +307,8 @@ private final void doRegistrationInternal(boolean forceRefresh) { * been persisted. */ private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() { - CrossProcessLock lock = CrossProcessLock - .acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID); + CrossProcessLock lock = + CrossProcessLock.acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID); try { synchronized (lockGenerateFid) { PersistedInstallationEntry prefs = diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java index e572f734a79..704ef19f3b6 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java @@ -68,14 +68,14 @@ private static String getDefaultSenderId(FirebaseApp app) { return senderId; } String appId = app.getOptions().getApplicationId(); - if (!appId.startsWith("1:")) { - // Not v1, server should be updated to accept the full app ID now + if (!appId.startsWith("1:") && !appId.startsWith("2:")) { + // If applicationId does not contain a (GMP-)App-ID, it contains a Sender identifier return appId; } // For v1 app IDs, fall back to parsing the project number out @SuppressWarnings("StringSplitter") String[] parts = appId.split(":"); - if (parts.length < 2) { + if (parts.length != 4) { return null; // Invalid format } String projectNumber = parts[1];