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/17] 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 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 02/17] [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 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 03/17] 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 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 04/17] 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 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 05/17] 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 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 06/17] 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 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 07/17] 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 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 08/17] 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 f4ba63c6ae1988d43445b4fe21654890e5f210a2 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Tue, 6 Aug 2019 13:43:47 -0700 Subject: [PATCH 09/17] 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: Tue, 10 Dec 2019 11:26:26 -0800 Subject: [PATCH 10/17] Disable registrar test for FirebaseSegmentation. (#1050) * 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 * Disable registrar test for FirebaseSegmentation. --- .../segmentation/FirebaseSegmentationRegistrarTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 56b0d120eb0..b45fd92b12c 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 @@ -20,6 +20,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -32,6 +33,8 @@ public void setUp() { FirebaseApp.clearInstancesForTest(); } + // TODO(rgowman:b/123870630): Enable test. + @Ignore @Test public void getFirebaseInstallationsInstance() { FirebaseApp defaultApp = From fd0a4faf9085a0f0d5fc598a360b9c433b0c91a1 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Fri, 13 Dec 2019 16:16:06 -0800 Subject: [PATCH 11/17] Replace the custom installation id cache SharedPref implementation by file implementation. (#1056) * 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 * Disable registrar test for FirebaseSegmentation. * Disable test lab * Add api info for segmentation API * [FLoC] Replace the custom installation id cache SharedPref implementation by file implementation. --- firebase-segmentation/api.txt | 72 +++++++++++++ .../firebase-segmentation.gradle | 8 +- .../local/CustomInstallationIdCache.java | 102 ++++++++++++------ 3 files changed, 145 insertions(+), 37 deletions(-) create mode 100644 firebase-segmentation/api.txt diff --git a/firebase-segmentation/api.txt b/firebase-segmentation/api.txt new file mode 100644 index 00000000000..e1469393eaf --- /dev/null +++ b/firebase-segmentation/api.txt @@ -0,0 +1,72 @@ +// Signature format: 2.0 +package com.google.firebase.segmentation { + + public class FirebaseSegmentation { + method @NonNull public static com.google.firebase.segmentation.FirebaseSegmentation getInstance(); + method @NonNull public static com.google.firebase.segmentation.FirebaseSegmentation getInstance(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public com.google.android.gms.tasks.Task setCustomInstallationId(@Nullable String); + field public static final String TAG = "FirebaseSegmentation"; + } + + public class FirebaseSegmentationRegistrar implements com.google.firebase.components.ComponentRegistrar { + ctor public FirebaseSegmentationRegistrar(); + method @NonNull public java.util.List> getComponents(); + } + + public class SetCustomInstallationIdException extends com.google.firebase.FirebaseException { + method @NonNull public com.google.firebase.segmentation.SetCustomInstallationIdException.Status getStatus(); + } + + public enum SetCustomInstallationIdException.Status { + enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status BACKEND_ERROR; + enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status CLIENT_ERROR; + enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status DUPLICATED_CUSTOM_INSTALLATION_ID; + enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status UNKOWN; + } + +} + +package com.google.firebase.segmentation.local { + + public class CustomInstallationIdCache { + ctor public CustomInstallationIdCache(@NonNull com.google.firebase.FirebaseApp); + method @NonNull public boolean clear(); + method @NonNull public boolean insertOrUpdateCacheEntry(@NonNull com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue); + method @Nullable public com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue readCacheEntryValue(); + } + + public enum CustomInstallationIdCache.CacheStatus { + enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus PENDING_CLEAR; + enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus PENDING_UPDATE; + enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus SYNCED; + } + + public abstract class CustomInstallationIdCacheEntryValue { + ctor public CustomInstallationIdCacheEntryValue(); + method @NonNull public static com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue create(@NonNull String, @NonNull String, @NonNull com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus); + method @NonNull public abstract com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus getCacheStatus(); + method @NonNull public abstract String getCustomInstallationId(); + method @NonNull public abstract String getFirebaseInstanceId(); + } + +} + +package com.google.firebase.segmentation.remote { + + public class SegmentationServiceClient { + ctor public SegmentationServiceClient(@NonNull android.content.Context); + method @NonNull public com.google.firebase.segmentation.remote.SegmentationServiceClient.Code clearCustomInstallationId(long, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public com.google.firebase.segmentation.remote.SegmentationServiceClient.Code updateCustomInstallationId(long, @NonNull String, @NonNull String, @NonNull String, @NonNull String); + } + + public enum SegmentationServiceClient.Code { + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code CONFLICT; + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code HTTP_CLIENT_ERROR; + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code NETWORK_ERROR; + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code OK; + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code SERVER_ERROR; + enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code UNAUTHORIZED; + } + +} + diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index c11085bd161..c07201f2a77 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -16,10 +16,6 @@ plugins { id 'firebase-library' } -firebaseLibrary { - testLab.enabled = true -} - android { compileSdkVersion project.targetSdkVersion @@ -48,7 +44,7 @@ dependencies { exclude group: "com.google.firebase", module: "firebase-common" } - implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.0' @@ -59,7 +55,7 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation "org.robolectric:robolectric:$robolectricVersion" - androidTestImplementation "androidx.annotation:annotation:1.0.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' 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 307d5d49923..eb77445bf70 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 @@ -14,11 +14,16 @@ 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; +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 caches a few Firebase Segmentation attributes on top the Segmentation @@ -40,59 +45,94 @@ public enum CacheStatus { PENDING_CLEAR } - private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; + private static final String DATA_FILE_NAME_PREFIX = "PersistedCustomInstallationId"; 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; + private final File dataFile; + private final 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 = - firebaseApp - .getApplicationContext() - .getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - persistenceKey = firebaseApp.getPersistenceKey(); + this.firebaseApp = firebaseApp; + // Store custom installation id in different file for different FirebaseApp. + dataFile = + new File( + firebaseApp.getApplicationContext().getFilesDir(), + String.format("%s.%s.json", DATA_FILE_NAME_PREFIX, 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); + JSONObject cidInfo = readCidInfoFromFile(); + String cid = cidInfo.optString(CUSTOM_INSTALLATION_ID_KEY, null); + String iid = cidInfo.optString(INSTANCE_ID_KEY, null); + int status = cidInfo.optInt(CACHE_STATUS_KEY, -1); if (cid == null || iid == null || status == -1) { return null; } - return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); } + private JSONObject readCidInfoFromFile() { + 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 + * DATA_FILE_NAME. + */ @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(); + try { + // Write the prefs into a JSON object + JSONObject json = new JSONObject(); + json.put(CUSTOM_INSTALLATION_ID_KEY, entryValue.getCustomInstallationId()); + json.put(INSTANCE_ID_KEY, entryValue.getFirebaseInstanceId()); + json.put(CACHE_STATUS_KEY, entryValue.getCacheStatus().ordinal()); + File tmpFile = + File.createTempFile( + String.format("%s.%s", DATA_FILE_NAME_PREFIX, firebaseApp.getPersistenceKey()), + "tmp", + firebaseApp.getApplicationContext().getFilesDir()); + + // Serialize 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 " + dataFile.getPath()); + } + } catch (JSONException | IOException e) { + return false; + } + return true; } @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); + if (!dataFile.exists()) { + return true; + } + return dataFile.delete(); } } From 6e37fb99155f6f844281859edbd786989d7549b5 Mon Sep 17 00:00:00 2001 From: Di Wu <49409954+diwu-arete@users.noreply.github.com> Date: Fri, 13 Dec 2019 23:10:22 -0800 Subject: [PATCH 12/17] [FLoC] Change FLoC instrumental tests to be unit tests. (#1058) * [FLoC] Change FLoC instrumental tests to be unit tests. --- .../firebase-segmentation.gradle | 12 +--- .../src/androidTest/AndroidManifest.xml | 26 ------- .../FirebaseSegmentationTest.java} | 65 ++++++++++++----- .../segmentation/TestOnCompleteListener.java | 70 +++++++++++++++++++ .../local/CustomInstallationIdCacheTest.java | 4 +- 5 files changed, 123 insertions(+), 54 deletions(-) delete mode 100644 firebase-segmentation/src/androidTest/AndroidManifest.xml rename firebase-segmentation/src/{androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java => test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java} (80%) create mode 100644 firebase-segmentation/src/test/java/com/google/firebase/segmentation/TestOnCompleteListener.java rename firebase-segmentation/src/{androidTest => test}/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java (97%) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index c07201f2a77..14b4deb8018 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -54,13 +54,7 @@ dependencies { 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" - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' + testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation 'org.mockito:mockito-inline:2.25.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/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java similarity index 80% rename from firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java rename to firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java index 034ee1eb339..701bfcab06f 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/FirebaseSegmentationInstrumentedTest.java +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java @@ -17,14 +17,13 @@ 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.any; 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; @@ -34,6 +33,10 @@ import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; import com.google.firebase.segmentation.remote.SegmentationServiceClient; 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; @@ -42,16 +45,11 @@ import org.junit.runners.MethodSorters; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) +@RunWith(RobolectricTestRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class FirebaseSegmentationInstrumentedTest { - +public class FirebaseSegmentationTest { private static final String CUSTOM_INSTALLATION_ID = "123"; private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; @@ -59,9 +57,12 @@ public class FirebaseSegmentationInstrumentedTest { @Mock private FirebaseInstanceId firebaseInstanceId; @Mock private SegmentationServiceClient backendClientReturnsOk; @Mock private SegmentationServiceClient backendClientReturnsError; + private CustomInstallationIdCache actualCache; @Mock private CustomInstallationIdCache cacheReturnsError; + private ExecutorService taskExecutor; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -105,6 +106,8 @@ public String getToken() { })); when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false); when(cacheReturnsError.readCacheEntryValue()).thenReturn(null); + + taskExecutor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); } @After @@ -119,7 +122,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); // No exception, means success. - assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID))); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(CUSTOM_INSTALLATION_ID) + .addOnCompleteListener(taskExecutor, onCompleteListener); + assertNull(onCompleteListener.await()); CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); @@ -135,7 +142,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() // Expect exception try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(CUSTOM_INSTALLATION_ID) + .addOnCompleteListener(taskExecutor, onCompleteListener); + onCompleteListener.await(); fail(); } catch (ExecutionException expected) { Throwable cause = expected.getCause(); @@ -163,7 +174,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() // Expect exception try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(CUSTOM_INSTALLATION_ID) + .addOnCompleteListener(taskExecutor, onCompleteListener); + onCompleteListener.await(); fail(); } catch (ExecutionException expected) { Throwable cause = expected.getCause(); @@ -184,7 +199,11 @@ public void testUpdateCustomInstallationId_CacheError_BackendOk() throws Interru // Expect exception try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(CUSTOM_INSTALLATION_ID) + .addOnCompleteListener(taskExecutor, onCompleteListener); + onCompleteListener.await(); fail(); } catch (ExecutionException expected) { Throwable cause = expected.getCause(); @@ -206,7 +225,11 @@ public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); // No exception, means success. - assertNull(Tasks.await(firebaseSegmentation.setCustomInstallationId(null))); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(null) + .addOnCompleteListener(taskExecutor, onCompleteListener); + assertNull(onCompleteListener.await()); CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); assertNull(entryValue); } @@ -224,7 +247,11 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio // Expect exception try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(null)); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(null) + .addOnCompleteListener(taskExecutor, onCompleteListener); + onCompleteListener.await(); fail(); } catch (ExecutionException expected) { Throwable cause = expected.getCause(); @@ -248,7 +275,11 @@ public void testClearCustomInstallationId_CacheError_BackendOk() throws Interrup // Expect exception try { - Tasks.await(firebaseSegmentation.setCustomInstallationId(CUSTOM_INSTALLATION_ID)); + TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); + firebaseSegmentation + .setCustomInstallationId(null) + .addOnCompleteListener(taskExecutor, onCompleteListener); + onCompleteListener.await(); fail(); } catch (ExecutionException expected) { Throwable cause = expected.getCause(); diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/TestOnCompleteListener.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/TestOnCompleteListener.java new file mode 100644 index 00000000000..b81cbb8e485 --- /dev/null +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/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.segmentation; + +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 SetCustomInstallationIdException) { + throw new ExecutionException(exception); + } + if (exception instanceof IOException) { + throw new ExecutionException(exception); + } + throw new IllegalStateException("got an unexpected exception type", exception); + } + } +} diff --git a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java similarity index 97% rename from firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java rename to firebase-segmentation/src/test/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java index 9e22e522d2b..66c2e6f3928 100644 --- a/firebase-segmentation/src/androidTest/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/local/CustomInstallationIdCacheTest.java @@ -19,16 +19,16 @@ 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; +import org.robolectric.RobolectricTestRunner; /** Instrumented tests for {@link CustomInstallationIdCache} */ -@RunWith(AndroidJUnit4.class) +@RunWith(RobolectricTestRunner.class) public class CustomInstallationIdCacheTest { private FirebaseApp firebaseApp0; From e65c3b0b05900315b5f5b8f30a099d776481073e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 23 Jan 2020 11:06:37 -0800 Subject: [PATCH 13/17] Hide non-public classes. --- firebase-segmentation/api.txt | 51 +------------------ .../FirebaseSegmentationRegistrar.java | 1 + .../SetCustomInstallationIdException.java | 2 +- .../google/firebase/segmentation/Utils.java | 6 ++- .../local/CustomInstallationIdCache.java | 2 + .../CustomInstallationIdCacheEntryValue.java | 2 + .../remote/SegmentationServiceClient.java | 6 ++- subprojects.cfg | 2 +- 8 files changed, 18 insertions(+), 54 deletions(-) diff --git a/firebase-segmentation/api.txt b/firebase-segmentation/api.txt index e1469393eaf..e5feed1804b 100644 --- a/firebase-segmentation/api.txt +++ b/firebase-segmentation/api.txt @@ -8,11 +8,6 @@ package com.google.firebase.segmentation { field public static final String TAG = "FirebaseSegmentation"; } - public class FirebaseSegmentationRegistrar implements com.google.firebase.components.ComponentRegistrar { - ctor public FirebaseSegmentationRegistrar(); - method @NonNull public java.util.List> getComponents(); - } - public class SetCustomInstallationIdException extends com.google.firebase.FirebaseException { method @NonNull public com.google.firebase.segmentation.SetCustomInstallationIdException.Status getStatus(); } @@ -21,51 +16,7 @@ package com.google.firebase.segmentation { enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status BACKEND_ERROR; enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status CLIENT_ERROR; enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status DUPLICATED_CUSTOM_INSTALLATION_ID; - enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status UNKOWN; - } - -} - -package com.google.firebase.segmentation.local { - - public class CustomInstallationIdCache { - ctor public CustomInstallationIdCache(@NonNull com.google.firebase.FirebaseApp); - method @NonNull public boolean clear(); - method @NonNull public boolean insertOrUpdateCacheEntry(@NonNull com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue); - method @Nullable public com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue readCacheEntryValue(); - } - - public enum CustomInstallationIdCache.CacheStatus { - enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus PENDING_CLEAR; - enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus PENDING_UPDATE; - enum_constant public static final com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus SYNCED; - } - - public abstract class CustomInstallationIdCacheEntryValue { - ctor public CustomInstallationIdCacheEntryValue(); - method @NonNull public static com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue create(@NonNull String, @NonNull String, @NonNull com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus); - method @NonNull public abstract com.google.firebase.segmentation.local.CustomInstallationIdCache.CacheStatus getCacheStatus(); - method @NonNull public abstract String getCustomInstallationId(); - method @NonNull public abstract String getFirebaseInstanceId(); - } - -} - -package com.google.firebase.segmentation.remote { - - public class SegmentationServiceClient { - ctor public SegmentationServiceClient(@NonNull android.content.Context); - method @NonNull public com.google.firebase.segmentation.remote.SegmentationServiceClient.Code clearCustomInstallationId(long, @NonNull String, @NonNull String, @NonNull String); - method @NonNull public com.google.firebase.segmentation.remote.SegmentationServiceClient.Code updateCustomInstallationId(long, @NonNull String, @NonNull String, @NonNull String, @NonNull String); - } - - public enum SegmentationServiceClient.Code { - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code CONFLICT; - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code HTTP_CLIENT_ERROR; - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code NETWORK_ERROR; - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code OK; - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code SERVER_ERROR; - enum_constant public static final com.google.firebase.segmentation.remote.SegmentationServiceClient.Code UNAUTHORIZED; + enum_constant public static final com.google.firebase.segmentation.SetCustomInstallationIdException.Status UNKNOWN; } } 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 ca7f688d60a..bc30ab3bba6 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 @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.List; +/** @hide */ public class FirebaseSegmentationRegistrar implements ComponentRegistrar { @Override 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 2291c078a03..1d0826d39d9 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 @@ -21,7 +21,7 @@ public class SetCustomInstallationIdException extends FirebaseException { public enum Status { - UNKOWN(0), + UNKNOWN(0), /** Error in Firebase SDK. */ CLIENT_ERROR(1), 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 index ca231a89cb5..e6d03fabb2e 100644 --- a/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java +++ b/firebase-segmentation/src/main/java/com/google/firebase/segmentation/Utils.java @@ -17,7 +17,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Util methods used for {@link FirebaseSegmentation} */ +/** + * Util methods used for {@link FirebaseSegmentation} + * + * @hide + */ class Utils { private static final Pattern APP_ID_PATTERN = 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 eb77445bf70..675bf1d0472 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 @@ -28,6 +28,8 @@ /** * A layer that locally caches a few Firebase Segmentation attributes on top the Segmentation * backend API. + * + * @hide */ public class CustomInstallationIdCache { 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 5e2c1944278..5d67df1332b 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 @@ -21,6 +21,8 @@ /** * 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. + * + * @hide */ @AutoValue public abstract class CustomInstallationIdCacheEntryValue { 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 65058bb97da..9e550b6daa6 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 @@ -29,7 +29,11 @@ import org.json.JSONException; import org.json.JSONObject; -/** Http client that sends request to Firebase Segmentation backend API. To be implemented */ +/** + * Http client that sends request to Firebase Segmentation backend API. To be implemented + * + * @hide + */ public class SegmentationServiceClient { private static final String FIREBASE_SEGMENTATION_API_DOMAIN = diff --git a/subprojects.cfg b/subprojects.cfg index d025734d440..531ddce7f68 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -25,10 +25,10 @@ firebase-inappmessaging-display firebase-inappmessaging-display:ktx firebase-installations-interop firebase-installations +firebase-segmentation firebase-storage firebase-storage:ktx firebase-storage:test-app -firebase-segmentation protolite-well-known-types encoders From fd3a20f9839429a332a67b3d3fc9246cdb4480a9 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 30 Mar 2020 12:37:06 -0700 Subject: [PATCH 14/17] Firebase Segmentation SDK switches to depend on FIS. --- .../firebase-segmentation.gradle | 4 +- firebase-segmentation/gradle.properties | 2 +- .../segmentation/FirebaseSegmentation.java | 62 +++++++++---------- .../FirebaseSegmentationRegistrar.java | 7 ++- .../FirebaseSegmentationTest.java | 59 +++++++++++------- 5 files changed, 77 insertions(+), 57 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 1f8dfda7426..bad46c716b1 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -40,8 +40,8 @@ android { dependencies { implementation project(':firebase-common') implementation project(':firebase-components') - - implementation('com.google.firebase:firebase-iid:17.0.3') { + + implementation('com.google.firebase:firebase-installations-interop:16.0.0') { exclude group: "com.google.firebase", module: "firebase-common" } diff --git a/firebase-segmentation/gradle.properties b/firebase-segmentation/gradle.properties index 752913a3eb5..ba39c95ac29 100644 --- a/firebase-segmentation/gradle.properties +++ b/firebase-segmentation/gradle.properties @@ -1 +1 @@ -version=17.1.1 +version=17.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 b1d5e6e6742..d9d00e67e16 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 @@ -21,8 +21,8 @@ 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.installations.FirebaseInstallationsApi; +import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.segmentation.SetCustomInstallationIdException.Status; import com.google.firebase.segmentation.local.CustomInstallationIdCache; import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; @@ -38,26 +38,26 @@ public class FirebaseSegmentation { public static final String TAG = "FirebaseSegmentation"; private final FirebaseApp firebaseApp; - private final FirebaseInstanceId firebaseInstanceId; + private final FirebaseInstallationsApi firebaseInstallationsApi; private final CustomInstallationIdCache localCache; private final SegmentationServiceClient backendServiceClient; private final Executor executor; - FirebaseSegmentation(FirebaseApp firebaseApp) { + FirebaseSegmentation(FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallationsApi) { this( firebaseApp, - FirebaseInstanceId.getInstance(firebaseApp), + firebaseInstallationsApi, new CustomInstallationIdCache(firebaseApp), new SegmentationServiceClient(firebaseApp.getApplicationContext())); } FirebaseSegmentation( FirebaseApp firebaseApp, - FirebaseInstanceId firebaseInstanceId, + FirebaseInstallationsApi firebaseInstallationsApi, CustomInstallationIdCache localCache, SegmentationServiceClient backendServiceClient) { this.firebaseApp = firebaseApp; - this.firebaseInstanceId = firebaseInstanceId; + this.firebaseInstallationsApi = firebaseInstallationsApi; this.localCache = localCache; this.backendServiceClient = backendServiceClient; this.executor = Executors.newFixedThreadPool(4); @@ -82,7 +82,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); } @@ -125,20 +125,21 @@ private Void updateCustomInstallationId(String customInstallationId) return null; } - InstanceIdResult instanceIdResult; + String fid; + InstallationTokenResult installationTokenResult; try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + fid = Tasks.await(firebaseInstallationsApi.getId()); + // No need to force refresh token. + installationTokenResult = Tasks.await(firebaseInstallationsApi.getToken(false)); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to get Firebase instance id"); + Status.CLIENT_ERROR, "Failed to get Firebase installation ID and token"); } boolean firstUpdateCacheResult = localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); + customInstallationId, fid, CustomInstallationIdCache.CacheStatus.PENDING_UPDATE)); if (!firstUpdateCacheResult) { throw new SetCustomInstallationIdException( @@ -146,15 +147,13 @@ private Void updateCustomInstallationId(String customInstallationId) } // 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); + fid, + installationTokenResult.getToken()); boolean finalUpdateCacheResult; switch (backendRequestResult) { @@ -162,9 +161,7 @@ private Void updateCustomInstallationId(String customInstallationId) finalUpdateCacheResult = localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - customInstallationId, - instanceIdResult.getId(), - CustomInstallationIdCache.CacheStatus.SYNCED)); + customInstallationId, fid, CustomInstallationIdCache.CacheStatus.SYNCED)); break; case UNAUTHORIZED: localCache.clear(); @@ -174,14 +171,16 @@ private Void updateCustomInstallationId(String customInstallationId) localCache.clear(); throw new SetCustomInstallationIdException( Status.DUPLICATED_CUSTOM_INSTALLATION_ID, - "The custom installation id is used by another Firebase installation in your project."); + "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. + // These are considered retryable errors, so not to clean up + // the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } @@ -212,32 +211,33 @@ private Void updateCustomInstallationId(String customInstallationId) */ @WorkerThread private Void clearCustomInstallationId() throws SetCustomInstallationIdException { - InstanceIdResult instanceIdResult; + String fid; + InstallationTokenResult installationTokenResult; try { - instanceIdResult = Tasks.await(firebaseInstanceId.getInstanceId()); + fid = Tasks.await(firebaseInstallationsApi.getId()); + // No need to force refresh token. + installationTokenResult = Tasks.await(firebaseInstallationsApi.getToken(false)); } catch (ExecutionException | InterruptedException e) { throw new SetCustomInstallationIdException( - Status.CLIENT_ERROR, "Failed to get Firebase instance id"); + Status.CLIENT_ERROR, "Failed to get Firebase installation ID and token"); } boolean firstUpdateCacheResult = localCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( - "", instanceIdResult.getId(), CustomInstallationIdCache.CacheStatus.PENDING_CLEAR)); + "", fid, 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); + fid, + installationTokenResult.getToken()); boolean finalUpdateCacheResult; switch (backendRequestResult) { 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 bc30ab3bba6..c69385a9cba 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 @@ -19,6 +19,7 @@ import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; @@ -32,7 +33,11 @@ public List> getComponents() { return Arrays.asList( Component.builder(FirebaseSegmentation.class) .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new FirebaseSegmentation(c.get(FirebaseApp.class))) + .add(Dependency.required(FirebaseInstallationsApi.class)) + .factory( + c -> + new FirebaseSegmentation( + c.get(FirebaseApp.class), c.get(FirebaseInstallationsApi.class))) .build(), LibraryVersionComponent.create("fire-segmentation", BuildConfig.VERSION_NAME)); } diff --git a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java index 701bfcab06f..6a8a37485c2 100644 --- a/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java +++ b/firebase-segmentation/src/test/java/com/google/firebase/segmentation/FirebaseSegmentationTest.java @@ -27,8 +27,8 @@ 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.installations.FirebaseInstallationsApi; +import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.segmentation.local.CustomInstallationIdCache; import com.google.firebase.segmentation.local.CustomInstallationIdCacheEntryValue; import com.google.firebase.segmentation.remote.SegmentationServiceClient; @@ -44,6 +44,7 @@ import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -51,10 +52,11 @@ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class FirebaseSegmentationTest { private static final String CUSTOM_INSTALLATION_ID = "123"; - private static final String FIREBASE_INSTANCE_ID = "cAAAAAAAAAA"; + private static final String FIREBASE_INSTALLATION_ID = "fid_is_better_than_iid"; + private static final String FIREBASE_INSTALLATION_ID_TOKEN = "fis_token"; private FirebaseApp firebaseApp; - @Mock private FirebaseInstanceId firebaseInstanceId; + @Mock private FirebaseInstallationsApi firebaseInstallationsApi; @Mock private SegmentationServiceClient backendClientReturnsOk; @Mock private SegmentationServiceClient backendClientReturnsError; @@ -88,20 +90,33 @@ public void setUp() { when(backendClientReturnsError.clearCustomInstallationId( anyLong(), anyString(), anyString(), anyString())) .thenReturn(SegmentationServiceClient.Code.SERVER_ERROR); - when(firebaseInstanceId.getInstanceId()) + when(firebaseInstallationsApi.getId()).thenReturn(Tasks.forResult(FIREBASE_INSTALLATION_ID)); + when(firebaseInstallationsApi.getToken(Mockito.anyBoolean())) .thenReturn( Tasks.forResult( - new InstanceIdResult() { + new InstallationTokenResult() { @NonNull @Override - public String getId() { - return FIREBASE_INSTANCE_ID; + public String getToken() { + return FIREBASE_INSTALLATION_ID_TOKEN; } @NonNull @Override - public String getToken() { - return "iid_token"; + public long getTokenExpirationTimestamp() { + return 0; + } + + @NonNull + @Override + public long getTokenCreationTimestamp() { + return 0; + } + + @NonNull + @Override + public Builder toBuilder() { + return null; } })); when(cacheReturnsError.insertOrUpdateCacheEntry(any())).thenReturn(false); @@ -119,7 +134,7 @@ public void cleanUp() throws Exception { public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsOk); // No exception, means success. TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); @@ -129,7 +144,7 @@ public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception assertNull(onCompleteListener.await()); CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTALLATION_ID); assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); } @@ -138,7 +153,7 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); // Expect exception try { @@ -157,7 +172,7 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId()).isEqualTo(CUSTOM_INSTALLATION_ID); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTALLATION_ID); assertThat(entryValue.getCacheStatus()) .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_UPDATE); } @@ -170,7 +185,7 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() .thenReturn(SegmentationServiceClient.Code.CONFLICT); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); // Expect exception try { @@ -195,7 +210,7 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + firebaseApp, firebaseInstallationsApi, cacheReturnsError, backendClientReturnsOk); // Expect exception try { @@ -218,11 +233,11 @@ public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { actualCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, + FIREBASE_INSTALLATION_ID, CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsOk); + firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsOk); // No exception, means success. TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); @@ -239,11 +254,11 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio actualCache.insertOrUpdateCacheEntry( CustomInstallationIdCacheEntryValue.create( CUSTOM_INSTALLATION_ID, - FIREBASE_INSTANCE_ID, + FIREBASE_INSTALLATION_ID, CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, actualCache, backendClientReturnsError); + firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); // Expect exception try { @@ -262,7 +277,7 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio CustomInstallationIdCacheEntryValue entryValue = actualCache.readCacheEntryValue(); assertThat(entryValue.getCustomInstallationId().isEmpty()).isTrue(); - assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTANCE_ID); + assertThat(entryValue.getFirebaseInstanceId()).isEqualTo(FIREBASE_INSTALLATION_ID); assertThat(entryValue.getCacheStatus()) .isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING_CLEAR); } @@ -271,7 +286,7 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstanceId, cacheReturnsError, backendClientReturnsOk); + firebaseApp, firebaseInstallationsApi, cacheReturnsError, backendClientReturnsOk); // Expect exception try { From aab0d3fcdb93071101e1106bd87ae2925d790332 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 30 Mar 2020 12:57:27 -0700 Subject: [PATCH 15/17] Add FIS as a runtime dependency --- firebase-segmentation/firebase-segmentation.gradle | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index bad46c716b1..89ff1f4c21b 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -40,14 +40,12 @@ android { dependencies { implementation project(':firebase-common') implementation project(':firebase-components') - - implementation('com.google.firebase:firebase-installations-interop:16.0.0') { - exclude group: "com.google.firebase", module: "firebase-common" - } + implementation project(':firebase-installations-interop') + runtimeOnly project(':firebase-installations') implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.multidex:multidex:2.0.1' - implementation 'com.google.android.gms:play-services-tasks:17.0.0' + implementation 'com.google.android.gms:play-services-tasks:17.0.2' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" annotationProcessor "com.google.auto.value:auto-value:1.6.2" From 70de2e113c745dc7a1f19fc820906b0615c11ea6 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 30 Mar 2020 13:01:54 -0700 Subject: [PATCH 16/17] minor formatting --- .../google/firebase/segmentation/FirebaseSegmentation.java | 6 ++---- 1 file changed, 2 insertions(+), 4 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 d9d00e67e16..b7e70bfef69 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 @@ -171,16 +171,14 @@ private Void updateCustomInstallationId(String customInstallationId) localCache.clear(); throw new SetCustomInstallationIdException( Status.DUPLICATED_CUSTOM_INSTALLATION_ID, - "The custom installation id is used by another " - + "Firebase installation in your project."); + "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. + // These are considered retryable errors, so not to clean up the cache. throw new SetCustomInstallationIdException(Status.BACKEND_ERROR); } From 413a194b37eb0923469fd1a098ac3729c137e87e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 24 Sep 2020 10:46:20 -0700 Subject: [PATCH 17/17] Change version code of firebase segmentation SDK to be a beta version. Also remove some unneeded build dependencies. --- firebase-segmentation/firebase-segmentation.gradle | 2 -- firebase-segmentation/gradle.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 89ff1f4c21b..5941b9f8130 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -43,8 +43,6 @@ dependencies { implementation project(':firebase-installations-interop') runtimeOnly project(':firebase-installations') - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.multidex:multidex:2.0.1' implementation 'com.google.android.gms:play-services-tasks:17.0.2' compileOnly "com.google.auto.value:auto-value-annotations:1.6.5" diff --git a/firebase-segmentation/gradle.properties b/firebase-segmentation/gradle.properties index ba39c95ac29..29ae9e151c9 100644 --- a/firebase-segmentation/gradle.properties +++ b/firebase-segmentation/gradle.properties @@ -1 +1 @@ -version=17.2.0 +version=16.0.0-beta01