diff --git a/.github/workflows/api-information.yml b/.github/workflows/api-information.yml index b09ed2f3741..67e624524a7 100644 --- a/.github/workflows/api-information.yml +++ b/.github/workflows/api-information.yml @@ -17,15 +17,6 @@ jobs: java-version: 11 distribution: temurin cache: gradle - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up Python 3.10 uses: actions/setup-python@v4 with: diff --git a/.github/workflows/build-release-artifacts.yml b/.github/workflows/build-release-artifacts.yml new file mode 100644 index 00000000000..6271f124061 --- /dev/null +++ b/.github/workflows/build-release-artifacts.yml @@ -0,0 +1,25 @@ +name: Build Release Artifacts + +on: + workflow_dispatch: + pull_request: + branches: + - 'releases/**' + +jobs: + build-artifacts: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + - name: Perform gradle build + run: | + ./gradlew firebasePublish -PprojectsToPublish=firebase-firestore -PpublishMode=RELEASE -PincludeFireEscapeArtifacts=true + - name: Upload generated artifacts + uses: actions/upload-artifact@v2 + with: + name: release_artifacts + path: build/*.zip + retention-days: 5 diff --git a/.github/workflows/build-src-check.yml b/.github/workflows/build-src-check.yml index b5267d02bc1..825af4f9bef 100644 --- a/.github/workflows/build-src-check.yml +++ b/.github/workflows/build-src-check.yml @@ -14,13 +14,6 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3.0.2 - - name: Setup NDK - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - ln -sfn $ANDROID_SDK_ROOT/ndk/21.4.7075529 $ANDROID_NDK_ROOT - - name: Set up JDK 11 uses: actions/setup-java@v2 with: @@ -33,7 +26,7 @@ jobs: run: | ./gradlew -b buildSrc/build.gradle.kts -PenablePluginTests=true check - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + uses: EnricoMi/publish-unit-test-result-action@94ba6dbddef5ec4aa827fc275cf7d563bc4d398f with: files: "**/build/test-results/**/*.xml" check_name: "buildSrc Test Results" diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index e9caa0de682..e86b6f06c9a 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -4,8 +4,6 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: - - '*' push: branches: - master @@ -22,15 +20,6 @@ jobs: with: fetch-depth: 2 submodules: true - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up JDK 11 uses: actions/setup-java@v2 @@ -59,15 +48,6 @@ jobs: with: fetch-depth: 2 submodules: true - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up JDK 11 uses: actions/setup-java@v2 @@ -118,15 +98,6 @@ jobs: with: fetch-depth: 2 submodules: true - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up JDK 11 uses: actions/setup-java@v2 diff --git a/.github/workflows/copyright-check.yml b/.github/workflows/copyright-check.yml index eccfd470a56..5461e529253 100644 --- a/.github/workflows/copyright-check.yml +++ b/.github/workflows/copyright-check.yml @@ -21,6 +21,7 @@ jobs: -e py \ -e gradle \ -e java \ + -e kt \ -e groovy \ -e sh \ -e proto diff --git a/.github/workflows/create_releases.yml b/.github/workflows/create_releases.yml new file mode 100644 index 00000000000..aa3a5f5da59 --- /dev/null +++ b/.github/workflows/create_releases.yml @@ -0,0 +1,49 @@ +name: Create release + +on: + workflow_dispatch: + inputs: + name: + description: 'Release name' + required: true + type: string + +jobs: + create-branches: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Create base branch + uses: peterjgrainger/action-create-branch@c2800a3a9edbba2218da6861fa46496cf8f3195a + with: + branch: 'releases/${{ inputs.name }}' + - name: Create release branch + uses: peterjgrainger/action-create-branch@c2800a3a9edbba2218da6861fa46496cf8f3195a + with: + branch: 'releases/${{ inputs.name }}.release' + + create-pull-request: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Create release configuration template + run: | + git config user.name 'Create Release GA' + git config user.email 'noreply@google.com' + echo "[release]" > release.cfg + echo "name = ${{ inputs.name }}" >> release.cfg + echo "mode = RELEASE" >> release.cfg + echo "" >> release.cfg + echo "[modules]" >> release.cfg + echo "" >> release.cfg + git add release.cfg + git commit -a -m 'Create release config' + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + base: 'releases/${{ inputs.name }}' + branch: 'releases/${{ inputs.name }}.release' + title: '${{ inputs.name}} release' diff --git a/.github/workflows/diff-javadoc.yml b/.github/workflows/diff-javadoc.yml new file mode 100644 index 00000000000..9fbb1f382f6 --- /dev/null +++ b/.github/workflows/diff-javadoc.yml @@ -0,0 +1,67 @@ +name: Diff Javadoc + +on: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Make Dir + run: mkdir ~/diff + + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + submodules: true + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: temurin + cache: gradle + + - name: Changed Modules + id: changed-modules + run: | + git diff --name-only HEAD~1 | xargs printf -- '--changed-git-paths %s\n' | xargs ./gradlew writeChangedProjects --output-file-path=modules.json --only-firebase-sdks + echo "run=$(cat modules.json | sed "s/[]\"[]//g" | sed "s/,/\n/g" | xargs printf -- "%s:kotlinDoc ")" >> $GITHUB_OUTPUT + + - name: Build + run: ./gradlew ${{ steps.changed-modules.outputs.run }} + + - name: Move original docs + run: mv build ~/diff/modified + + - uses: actions/checkout@v3 + with: + ref: ${{ github.base_ref }} + + - name: Build + run: ./gradlew ${{ steps.changed-modules.outputs.run }} + + - name: Move modified docs + run: mv build ~/diff/original + + - name: Diff docs + run: > + `# Recursively diff directories, including new files, git style, with 3 lines of context` + diff -wEburN ~/diff/original ~/diff/modified + `# Remove the first line and new file signifier of the output` + | tail -n +2 + `# Replace the diff new file signifier with the end and start of a new codeblock` + | sed "s/^diff.*$/\`\`\`\\n\`\`\`diff/g" + `# Add a collapsable block, summary, and start the first code block on the first line` + | sed "1s/^/
\\nJavadoc Changes:<\/summary>\\n\\n\`\`\`diff\\n/" + `# Close the final code block and close the collapsable on the final line` + | sed "$ s/$/\\n\`\`\`\\n<\/details>/" + `# Write to diff.md for later` + > diff.md + + - name: Add comment + uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + with: + message-path: diff.md diff --git a/.github/workflows/fireci.yml b/.github/workflows/fireci.yml index 7b5f7109da0..8228fa10728 100644 --- a/.github/workflows/fireci.yml +++ b/.github/workflows/fireci.yml @@ -18,8 +18,10 @@ jobs: - uses: actions/checkout@v3.0.2 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.8' - run: | pip install -e "ci/fireci[test]" - run: | pytest ci/fireci + - run: | + mypy --config-file ci/fireci/setup.cfg ci/fireci/ diff --git a/.github/workflows/fireperf-e2e.yml b/.github/workflows/fireperf-e2e.yml index 73f135b5db0..80a6465079f 100644 --- a/.github/workflows/fireperf-e2e.yml +++ b/.github/workflows/fireperf-e2e.yml @@ -33,15 +33,6 @@ jobs: java-version: 11 distribution: temurin cache: gradle - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up Python 3.10 uses: actions/setup-python@v4 with: diff --git a/.github/workflows/health-metrics.yml b/.github/workflows/health-metrics.yml index 8984a33cf7c..fb67d3f726f 100644 --- a/.github/workflows/health-metrics.yml +++ b/.github/workflows/health-metrics.yml @@ -1,6 +1,16 @@ name: Health Metrics -on: [ pull_request, push ] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - master + # add other feature branches here + # TODO(yifany): support workflow_dispatch for metric tests (or only for startup time test) env: GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} @@ -8,7 +18,10 @@ env: jobs: coverage: name: Coverage - if: (github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -21,15 +34,6 @@ jobs: java-version: 11 distribution: temurin cache: gradle - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up Python 3.10 uses: actions/setup-python@v4 with: @@ -49,7 +53,10 @@ jobs: size: name: Size - if: (github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -62,15 +69,6 @@ jobs: java-version: 11 distribution: temurin cache: gradle - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - name: Set up Python 3.10 uses: actions/setup-python@v4 with: @@ -87,3 +85,49 @@ jobs: - name: Run size tests (post-submit) if: ${{ github.event_name == 'push' }} run: fireci binary_size + + startup_time: + name: Startup Time + if: | + (github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk') + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + submodules: true + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: temurin + cache: gradle + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: google-github-actions/auth@v0 + with: + credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' + - uses: google-github-actions/setup-gcloud@v0 + - name: Set up fireci + run: pip3 install -e ci/fireci + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + BENCHMARK_APP_LOCATION: health-metrics/benchmark/template/app/google-services.json + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > $BENCHMARK_APP_LOCATION + - name: Run startup-time tests (presubmit) + if: ${{ github.event_name == 'pull_request' }} + run: | + git diff --name-only HEAD~1 | \ + xargs printf -- '--changed-git-paths %s\n' | \ + xargs ./gradlew writeChangedProjects --output-file-path=modules.json + fireci macrobenchmark ci --pull-request --changed-modules-file modules.json + - name: Run startup-time tests (post-submit) + if: ${{ github.event_name == 'push' }} + run: | + fireci macrobenchmark ci --push diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000000..e5f52d413b1 --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,55 @@ +name: Jekyll with GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + paths: + - '.github/workflows/jekyll-gh-pages.yml' + - 'contributor-docs/**' + pull_request: + paths: + - '.github/workflows/jekyll-gh-pages.yml' + - 'contributor-docs/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v2 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./contributor-docs + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + + deploy: + if: ${{ github.event_name == 'push' && github.repository == 'firebase/firebase-android-sdk' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml new file mode 100644 index 00000000000..27e231cb6af --- /dev/null +++ b/.github/workflows/make-bom.yml @@ -0,0 +1,39 @@ +name: Make BoM + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - uses: actions/checkout@v3.0.2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: temurin + cache: gradle + + - name: Build + run: | + ./ci/run.sh \ + --artifact-target-dir=./logs/artifacts \ + --artifact-patterns=bom.zip \ + --artifact-patterns=bomReleaseNotes.md \ + --artifact-patterns=recipeVersionUpdate.txt \ + gradle \ + -- \ + --build-cache \ + buildBomZip + + - name: Upload generated artifacts + uses: actions/upload-artifact@v2 + with: + name: artifacts + path: ./logs/artifacts/ + retention-days: 5 diff --git a/.github/workflows/merge-to-main.yml b/.github/workflows/merge-to-main.yml new file mode 100644 index 00000000000..33c9f1e4a54 --- /dev/null +++ b/.github/workflows/merge-to-main.yml @@ -0,0 +1,40 @@ +name: Merge to main + +on: + pull_request: + branches: + - master + types: + - opened + - labeled + - unlabeled + +jobs: + pr-message: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 + with: + message: > + ### 📝 PRs merging into main branch + + **Our main branch should always be in a releasable state**. + If you are working on a larger change, or if you don't want + this change to see the light of the day just yet, consider + using a feature branch first, and only merge into the main + branch when the code complete and ready to be released. + + + **Add the 'main-merge-ack' label to your PR to confirm + merging into the main branch is intended.** + + - name: Label checker + if: "!contains( github.event.pull_request.labels.*.name, 'main-merge-ack')" + run: | + echo Missing 'main-merge-ack' label. Read the comment about merging to master in your PR for more information. + exit 1 + + - name: Success + run: exit 0 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 00000000000..bcef7d7d679 --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecards supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '45 11 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27 + with: + sarif_file: results.sarif diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 00513efd629..e6bbf78afe9 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -17,15 +17,6 @@ jobs: java-version: 11 distribution: temurin cache: gradle - - name: Set up NDK 21.4.7075529 - run: | - ANDROID_ROOT=/usr/local/lib/android - ANDROID_SDK_ROOT=${ANDROID_ROOT}/sdk - ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk-bundle - SDKMANAGER=${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager - echo "y" | $SDKMANAGER "ndk;21.4.7075529" - ln -sfn ${ANDROID_SDK_ROOT}/ndk/21.4.7075529 ${ANDROID_NDK_ROOT} - echo "ANDROID_NDK_HOME=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV - uses: google-github-actions/auth@v0 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} diff --git a/.github/workflows/update-cpp-sdk-on-release.yml b/.github/workflows/update-cpp-sdk-on-release.yml index 54c384bd491..cb310320331 100644 --- a/.github/workflows/update-cpp-sdk-on-release.yml +++ b/.github/workflows/update-cpp-sdk-on-release.yml @@ -62,7 +62,7 @@ jobs: ref: main - name: Get firebase-workflow-trigger token - uses: tibdex/github-app-token@v1 + uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c id: generate-token with: app_id: ${{ secrets.CPP_WORKFLOW_TRIGGER_APP_ID }} diff --git a/.gitignore b/.gitignore index 300f5bb4b2f..da3e77d46fe 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ firebase-crashlytics-ndk/.externalNativeBuild/ firebase-crashlytics-ndk/.cxx/ smoke-test-logs/ smoke-tests/build-debug-headGit-smoke-test -smoke-tests/firehorn.log \ No newline at end of file +smoke-tests/firehorn.log +macrobenchmark-output.json diff --git a/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md index e9a4b2de744..4de30c64161 100644 --- a/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md +++ b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Integrated the [app_check] Debug Testing SDK with Firebase Components. (#4436) # 16.1.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle index b3bc75f9f0c..df42b931870 100644 --- a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle +++ b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle @@ -31,6 +31,7 @@ android { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion versionName version + multiDexEnabled = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArgument "firebaseAppCheckDebugSecret", System.getenv("FIREBASE_APP_CHECK_DEBUG_SECRET") ?: '' } diff --git a/appcheck/firebase-appcheck-debug-testing/gradle.properties b/appcheck/firebase-appcheck-debug-testing/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck-debug-testing/gradle.properties +++ b/appcheck/firebase-appcheck-debug-testing/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml index 3426ff98472..6b1df41b525 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck-debug-testing/src/main/AndroidManifest.xml @@ -17,4 +17,11 @@ package="com.google.firebase.appcheck.debug.testing"> + + + + + diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java deleted file mode 100644 index b484fe89d63..00000000000 --- a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactoryHelper.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 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.appcheck.debug; - -import androidx.annotation.NonNull; - -/** - * Helper class used by {@link com.google.firebase.appcheck.debug.testing.DebugAppCheckTestHelper} - * in order to access the package-private {@link DebugAppCheckProviderFactory} constructor that - * takes in a debug secret. - * - * @hide - */ -public class DebugAppCheckProviderFactoryHelper { - @NonNull - public static DebugAppCheckProviderFactory createDebugAppCheckProviderFactory( - @NonNull String debugSecret) { - return new DebugAppCheckProviderFactory(debugSecret); - } -} diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java index 82562258bf3..279cb51b539 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelper.java @@ -15,13 +15,10 @@ package com.google.firebase.appcheck.debug.testing; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.test.platform.app.InstrumentationRegistry; import com.google.firebase.FirebaseApp; import com.google.firebase.appcheck.AppCheckProviderFactory; import com.google.firebase.appcheck.FirebaseAppCheck; import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; -import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactoryHelper; import com.google.firebase.appcheck.internal.DefaultFirebaseAppCheck; /** @@ -66,28 +63,16 @@ * */ public final class DebugAppCheckTestHelper { - private static final String DEBUG_SECRET_KEY = "firebaseAppCheckDebugSecret"; - - private final String debugSecret; - /** - * Creates a {@link DebugAppCheckTestHelper} instance with the debug secret obtained from {@link - * InstrumentationRegistry} arguments. + * Creates a {@link DebugAppCheckTestHelper} instance with a debug secret obtained from {@link + * androidx.test.platform.app.InstrumentationRegistry} arguments. */ @NonNull public static DebugAppCheckTestHelper fromInstrumentationArgs() { - String debugSecret = InstrumentationRegistry.getArguments().getString(DEBUG_SECRET_KEY); - return new DebugAppCheckTestHelper(debugSecret); + return new DebugAppCheckTestHelper(); } - @VisibleForTesting - static DebugAppCheckTestHelper fromString(String debugSecret) { - return new DebugAppCheckTestHelper(debugSecret); - } - - private DebugAppCheckTestHelper(String debugSecret) { - this.debugSecret = debugSecret; - } + private DebugAppCheckTestHelper() {} /** * Installs a {@link DebugAppCheckProviderFactory} to the default {@link FirebaseApp} and runs the @@ -109,8 +94,7 @@ public void withDebugProvider( (DefaultFirebaseAppCheck) FirebaseAppCheck.getInstance(firebaseApp); AppCheckProviderFactory currentAppCheckProviderFactory = firebaseAppCheck.getInstalledAppCheckProviderFactory(); - firebaseAppCheck.installAppCheckProviderFactory( - DebugAppCheckProviderFactoryHelper.createDebugAppCheckProviderFactory(debugSecret)); + firebaseAppCheck.installAppCheckProviderFactory(DebugAppCheckProviderFactory.getInstance()); try { runnable.run(); } catch (Throwable throwable) { diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java new file mode 100644 index 00000000000..c4c0caf74eb --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/DebugSecretProvider.java @@ -0,0 +1,35 @@ +// Copyright 2022 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.appcheck.debug.testing; + +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; + +/** @hide */ +public class DebugSecretProvider implements InternalDebugSecretProvider { + private static final String DEBUG_SECRET_KEY = "firebaseAppCheckDebugSecret"; + + DebugSecretProvider() {} + + /** + * Returns a debug secret from {@link InstrumentationRegistry} arguments to be used with the + * {@link com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider} in continuous + * integration testing flows. + */ + @Override + public String getDebugSecret() { + return InstrumentationRegistry.getArguments().getString(DEBUG_SECRET_KEY); + } +} diff --git a/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java new file mode 100644 index 00000000000..d1951beb185 --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/main/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrar.java @@ -0,0 +1,45 @@ +// Copyright 2022 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.appcheck.debug.testing; + +import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.appcheck.debug.BuildConfig; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.platforminfo.LibraryVersionComponent; +import java.util.Arrays; +import java.util.List; + +/** + * {@link ComponentRegistrar} for setting up FirebaseAppCheck debug testing's dependency injections + * in Firebase Android Components. + * + * @hide + */ +@KeepForSdk +public class FirebaseAppCheckDebugTestingRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check-debug-testing"; + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(DebugSecretProvider.class, (InternalDebugSecretProvider.class)) + .name(LIBRARY_NAME) + .factory((container) -> new DebugSecretProvider()) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + } +} diff --git a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java index 5e27218d25c..4f6bcdc7878 100644 --- a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java +++ b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/DebugAppCheckTestHelperTest.java @@ -35,10 +35,9 @@ public class DebugAppCheckTestHelperTest { private static final String PROJECT_ID = "projectId"; private static final String APP_ID = "appId"; private static final String OTHER_FIREBASE_APP_NAME = "otherFirebaseAppName"; - private static final String DEBUG_SECRET = "debugSecret"; private final DebugAppCheckTestHelper debugAppCheckTestHelper = - DebugAppCheckTestHelper.fromString(DEBUG_SECRET); + DebugAppCheckTestHelper.fromInstrumentationArgs(); @Before public void setUp() { diff --git a/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java new file mode 100644 index 00000000000..05eadd44d4b --- /dev/null +++ b/appcheck/firebase-appcheck-debug-testing/src/test/java/com/google/firebase/appcheck/debug/testing/FirebaseAppCheckDebugTestingRegistrarTest.java @@ -0,0 +1,38 @@ +// Copyright 2022 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.appcheck.debug.testing; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.components.Component; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckDebugTestingRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckDebugTestingRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckDebugTestingRegistrar registrar = new FirebaseAppCheckDebugTestingRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckDebugTestingComponent = components.get(0); + assertThat(appCheckDebugTestingComponent.getDependencies()).isEmpty(); + assertThat(appCheckDebugTestingComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-debug/CHANGELOG.md b/appcheck/firebase-appcheck-debug/CHANGELOG.md index b23c1898f40..6a55b73021e 100644 --- a/appcheck/firebase-appcheck-debug/CHANGELOG.md +++ b/appcheck/firebase-appcheck-debug/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. (#4431, #4449) +* [changed] Integrated the [app_check] Debug SDK with Firebase Components. (#4436) +* [changed] Moved Task continuations off the main thread. (#4453) # 16.1.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle index 80cad7da537..bbfce8aa822 100644 --- a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle +++ b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle @@ -41,6 +41,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(':appcheck:firebase-appcheck') @@ -49,6 +50,7 @@ dependencies { javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + testImplementation project(':integ-testing') testImplementation 'junit:junit:4.13-beta-2' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" diff --git a/appcheck/firebase-appcheck-debug/gradle.properties b/appcheck/firebase-appcheck-debug/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck-debug/gradle.properties +++ b/appcheck/firebase-appcheck-debug/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java index d2135dc78d8..7a7d04710ed 100644 --- a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/DebugAppCheckProviderFactory.java @@ -27,20 +27,7 @@ public class DebugAppCheckProviderFactory implements AppCheckProviderFactory { private static final DebugAppCheckProviderFactory instance = new DebugAppCheckProviderFactory(); - private String debugSecret; - - private DebugAppCheckProviderFactory() { - this.debugSecret = null; - } - - /** - * This constructor is package-private in order to prevent debug secrets from being hard-coded in - * application logic. This constructor is used by the firebase-appcheck-debug-testing SDK, to - * inject debug secrets in integration tests. - */ - DebugAppCheckProviderFactory(String debugSecret) { - this.debugSecret = debugSecret; - } + private DebugAppCheckProviderFactory() {} /** * Gets an instance of this class for installation into a {@link @@ -55,7 +42,8 @@ public static DebugAppCheckProviderFactory getInstance() { @NonNull @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new DebugAppCheckProvider(firebaseApp, debugSecret); + return firebaseApp.get(DebugAppCheckProvider.class); } } diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java index b2a525f161b..9eb96cd0891 100644 --- a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrar.java @@ -15,11 +15,19 @@ package com.google.firebase.appcheck.debug; import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * {@link ComponentRegistrar} for setting up FirebaseAppCheck debug's dependency injections in @@ -33,6 +41,27 @@ public class FirebaseAppCheckDebugRegistrar implements ComponentRegistrar { @Override public List> getComponents() { - return Arrays.asList(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(DebugAppCheckProvider.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.optionalProvider(InternalDebugSecretProvider.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + (container) -> + new DebugAppCheckProvider( + container.get(FirebaseApp.class), + container.getProvider(InternalDebugSecretProvider.class), + container.get(liteExecutor), + container.get(backgroundExecutor), + container.get(blockingExecutor))) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } } diff --git a/tools/errorprone/src/test/java/com/google/firebase/FirebaseApp.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java similarity index 61% rename from tools/errorprone/src/test/java/com/google/firebase/FirebaseApp.java rename to appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java index c1b2beafa88..7b3a196abc4 100644 --- a/tools/errorprone/src/test/java/com/google/firebase/FirebaseApp.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/InternalDebugSecretProvider.java @@ -1,4 +1,4 @@ -// Copyright 2018 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,11 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase; +package com.google.firebase.appcheck.debug; -/** Fake FirebaseApp for testing purposes. */ -public class FirebaseApp { - public T get(Class anInterface) { - return null; - } +import androidx.annotation.Nullable; + +/** + * An interface for obtaining a debug secret to be used with {@link + * com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider}. + * + * @hide + */ +public interface InternalDebugSecretProvider { + @Nullable + String getDebugSecret(); } diff --git a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java index 4f23d52951a..443b126db92 100644 --- a/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java +++ b/appcheck/firebase-appcheck-debug/src/main/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProvider.java @@ -18,22 +18,23 @@ import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; -import com.google.firebase.appcheck.internal.AppCheckTokenResponse; +import com.google.firebase.appcheck.debug.InternalDebugSecretProvider; import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.inject.Provider; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; public class DebugAppCheckProvider implements AppCheckProvider { @@ -41,18 +42,30 @@ public class DebugAppCheckProvider implements AppCheckProvider { private static final String UTF_8 = "UTF-8"; private final NetworkClient networkClient; - private final ExecutorService backgroundExecutor; + private final Executor liteExecutor; + private final Executor blockingExecutor; private final RetryManager retryManager; private final Task debugSecretTask; - public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String debugSecret) { + public DebugAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @NonNull Provider debugSecretProvider, + @Lightweight Executor liteExecutor, + @Background Executor backgroundExecutor, + @Blocking Executor blockingExecutor) { checkNotNull(firebaseApp); this.networkClient = new NetworkClient(firebaseApp); - this.backgroundExecutor = Executors.newCachedThreadPool(); + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = new RetryManager(); + + String debugSecret = null; + if (debugSecretProvider.get() != null) { + debugSecret = debugSecretProvider.get().getDebugSecret(); + } this.debugSecretTask = debugSecret == null - ? determineDebugSecret(firebaseApp, this.backgroundExecutor) + ? determineDebugSecret(firebaseApp, backgroundExecutor) : Tasks.forResult(debugSecret); } @@ -60,10 +73,12 @@ public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String DebugAppCheckProvider( @NonNull String debugSecret, @NonNull NetworkClient networkClient, - @NonNull ExecutorService backgroundExecutor, + @NonNull Executor liteExecutor, + @NonNull Executor blockingExecutor, @NonNull RetryManager retryManager) { this.networkClient = networkClient; - this.backgroundExecutor = backgroundExecutor; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = retryManager; this.debugSecretTask = Tasks.forResult(debugSecret); } @@ -71,7 +86,7 @@ public DebugAppCheckProvider(@NonNull FirebaseApp firebaseApp, @Nullable String @VisibleForTesting @NonNull static Task determineDebugSecret( - @NonNull FirebaseApp firebaseApp, @NonNull ExecutorService executor) { + @NonNull FirebaseApp firebaseApp, @NonNull Executor executor) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); executor.execute( () -> { @@ -96,31 +111,21 @@ static Task determineDebugSecret( @Override public Task getToken() { return debugSecretTask - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) throws Exception { - ExchangeDebugTokenRequest request = new ExchangeDebugTokenRequest(task.getResult()); - return Tasks.call( - backgroundExecutor, - () -> - networkClient.exchangeAttestationForAppCheckToken( - request.toJsonString().getBytes(UTF_8), - NetworkClient.DEBUG, - retryManager)); - } + .onSuccessTask( + liteExecutor, + debugSecret -> { + ExchangeDebugTokenRequest request = new ExchangeDebugTokenRequest(debugSecret); + return Tasks.call( + blockingExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(UTF_8), + NetworkClient.DEBUG, + retryManager)); }) - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); - } - // TODO: Surface more error details. - return Tasks.forException(task.getException()); - } - }); + .onSuccessTask( + liteExecutor, + response -> + Tasks.forResult(DefaultAppCheckToken.constructFromAppCheckTokenResponse(response))); } } diff --git a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java new file mode 100644 index 00000000000..70301ee2eab --- /dev/null +++ b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/FirebaseAppCheckDebugRegistrarTest.java @@ -0,0 +1,51 @@ +// Copyright 2022 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.appcheck.debug; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.components.Component; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckDebugRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckDebugRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckDebugRegistrar registrar = new FirebaseAppCheckDebugRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckDebugComponent = components.get(0); + assertThat(appCheckDebugComponent.getDependencies()) + .containsExactly( + Dependency.required(FirebaseApp.class), + Dependency.optionalProvider(InternalDebugSecretProvider.class), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Background.class, Executor.class)), + Dependency.required(Qualified.qualified(Blocking.class, Executor.class))); + assertThat(appCheckDebugComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java index 3d53b398fd2..d7680b94b56 100644 --- a/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java +++ b/appcheck/firebase-appcheck-debug/src/test/java/com/google/firebase/appcheck/debug/internal/DebugAppCheckProviderTest.java @@ -33,8 +33,9 @@ import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.concurrent.TestOnlyExecutors; import java.io.IOException; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executor; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -72,7 +73,10 @@ public class DebugAppCheckProviderTest { private StorageHelper storageHelper; private SharedPreferences sharedPreferences; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). + private Executor liteExecutor = MoreExecutors.directExecutor(); + private Executor backgroundExecutor = MoreExecutors.directExecutor(); + private Executor blockingExecutor = MoreExecutors.directExecutor(); @Before public void setup() { @@ -100,7 +104,12 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DebugAppCheckProvider(null, null); + new DebugAppCheckProvider( + null, + null, + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -135,7 +144,7 @@ public void exchangeDebugToken_onSuccess_setsTaskResult() throws Exception { DebugAppCheckProvider provider = new DebugAppCheckProvider( - DEBUG_SECRET, mockNetworkClient, backgroundExecutor, mockRetryManager); + DEBUG_SECRET, mockNetworkClient, liteExecutor, blockingExecutor, mockRetryManager); Task task = provider.getToken(); verify(mockNetworkClient) @@ -154,7 +163,7 @@ public void exchangeDebugToken_onFailure_setsTaskException() throws Exception { DebugAppCheckProvider provider = new DebugAppCheckProvider( - DEBUG_SECRET, mockNetworkClient, backgroundExecutor, mockRetryManager); + DEBUG_SECRET, mockNetworkClient, liteExecutor, blockingExecutor, mockRetryManager); Task task = provider.getToken(); verify(mockNetworkClient) diff --git a/appcheck/firebase-appcheck-interop/gradle.properties b/appcheck/firebase-appcheck-interop/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck-interop/gradle.properties +++ b/appcheck/firebase-appcheck-interop/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md index bdea5a89161..194323623cf 100644 --- a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md +++ b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. (#4431, #4449) +* [changed] Integrated the [app_check] Play Integrity SDK with Firebase Components. (#4436) +* [changed] Moved Task continuations off the main thread. (#4453) # 16.1.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle index 92dcf4806c9..185558a75c5 100644 --- a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -41,6 +41,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(':appcheck:firebase-appcheck') @@ -50,6 +51,7 @@ dependencies { javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + testImplementation project(':integ-testing') testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.4.6' testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/appcheck/firebase-appcheck-playintegrity/gradle.properties b/appcheck/firebase-appcheck-playintegrity/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck-playintegrity/gradle.properties +++ b/appcheck/firebase-appcheck-playintegrity/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java index b1af71a1651..56f0e091907 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrar.java @@ -15,11 +15,18 @@ package com.google.firebase.appcheck.playintegrity; import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.playintegrity.internal.PlayIntegrityAppCheckProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * {@link ComponentRegistrar} for setting up FirebaseAppCheck play integrity's dependency injections @@ -33,6 +40,22 @@ public class FirebaseAppCheckPlayIntegrityRegistrar implements ComponentRegistra @Override public List> getComponents() { - return Arrays.asList(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(PlayIntegrityAppCheckProvider.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + (container) -> + new PlayIntegrityAppCheckProvider( + container.get(FirebaseApp.class), + container.get(liteExecutor), + container.get(blockingExecutor))) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java index 743092d2892..db909eddcb9 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/PlayIntegrityAppCheckProviderFactory.java @@ -40,7 +40,8 @@ public static PlayIntegrityAppCheckProviderFactory getInstance() { @NonNull @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new PlayIntegrityAppCheckProvider(firebaseApp); + return firebaseApp.get(PlayIntegrityAppCheckProvider.class); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java index 13381af83a6..910aa3be2c7 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java +++ b/appcheck/firebase-appcheck-playintegrity/src/main/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProvider.java @@ -23,13 +23,14 @@ import com.google.android.play.core.integrity.IntegrityTokenRequest; import com.google.android.play.core.integrity.IntegrityTokenResponse; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; public class PlayIntegrityAppCheckProvider implements AppCheckProvider { @@ -38,15 +39,20 @@ public class PlayIntegrityAppCheckProvider implements AppCheckProvider { private final String projectNumber; private final IntegrityManager integrityManager; private final NetworkClient networkClient; - private final ExecutorService backgroundExecutor; + private final Executor liteExecutor; + private final Executor blockingExecutor; private final RetryManager retryManager; - public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { + public PlayIntegrityAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @Lightweight Executor liteExecutor, + @Blocking Executor blockingExecutor) { this( firebaseApp.getOptions().getGcmSenderId(), IntegrityManagerFactory.create(firebaseApp.getApplicationContext()), new NetworkClient(firebaseApp), - Executors.newCachedThreadPool(), + liteExecutor, + blockingExecutor, new RetryManager()); } @@ -55,12 +61,14 @@ public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { @NonNull String projectNumber, @NonNull IntegrityManager integrityManager, @NonNull NetworkClient networkClient, - @NonNull ExecutorService backgroundExecutor, + @NonNull Executor liteExecutor, + @NonNull Executor blockingExecutor, @NonNull RetryManager retryManager) { this.projectNumber = projectNumber; this.integrityManager = integrityManager; this.networkClient = networkClient; - this.backgroundExecutor = backgroundExecutor; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = retryManager; } @@ -69,11 +77,12 @@ public PlayIntegrityAppCheckProvider(@NonNull FirebaseApp firebaseApp) { public Task getToken() { return getPlayIntegrityAttestation() .onSuccessTask( + liteExecutor, integrityTokenResponse -> { ExchangePlayIntegrityTokenRequest request = new ExchangePlayIntegrityTokenRequest(integrityTokenResponse.token()); return Tasks.call( - backgroundExecutor, + blockingExecutor, () -> networkClient.exchangeAttestationForAppCheckToken( request.toJsonString().getBytes(UTF_8), @@ -81,30 +90,30 @@ public Task getToken() { retryManager)); }) .onSuccessTask( - appCheckTokenResponse -> { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(appCheckTokenResponse)); - }); + liteExecutor, + appCheckTokenResponse -> + Tasks.forResult( + DefaultAppCheckToken.constructFromAppCheckTokenResponse( + appCheckTokenResponse))); } @NonNull private Task getPlayIntegrityAttestation() { GeneratePlayIntegrityChallengeRequest generateChallengeRequest = new GeneratePlayIntegrityChallengeRequest(); - Task generateChallengeTask = - Tasks.call( - backgroundExecutor, + return Tasks.call( + blockingExecutor, () -> GeneratePlayIntegrityChallengeResponse.fromJsonString( networkClient.generatePlayIntegrityChallenge( - generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager))); - return generateChallengeTask.onSuccessTask( - generatePlayIntegrityChallengeResponse -> { - return integrityManager.requestIntegrityToken( - IntegrityTokenRequest.builder() - .setCloudProjectNumber(Long.parseLong(projectNumber)) - .setNonce(generatePlayIntegrityChallengeResponse.getChallenge()) - .build()); - }); + generateChallengeRequest.toJsonString().getBytes(UTF_8), retryManager))) + .onSuccessTask( + liteExecutor, + generatePlayIntegrityChallengeResponse -> + integrityManager.requestIntegrityToken( + IntegrityTokenRequest.builder() + .setCloudProjectNumber(Long.parseLong(projectNumber)) + .setNonce(generatePlayIntegrityChallengeResponse.getChallenge()) + .build())); } } diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java new file mode 100644 index 00000000000..4b2f7a632ba --- /dev/null +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/FirebaseAppCheckPlayIntegrityRegistrarTest.java @@ -0,0 +1,48 @@ +// Copyright 2022 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.appcheck.playintegrity; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.components.Component; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckPlayIntegrityRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckPlayIntegrityRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckPlayIntegrityRegistrar registrar = new FirebaseAppCheckPlayIntegrityRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckPlayIntegrityComponent = components.get(0); + assertThat(appCheckPlayIntegrityComponent.getDependencies()) + .containsExactly( + Dependency.required(FirebaseApp.class), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Blocking.class, Executor.class))); + assertThat(appCheckPlayIntegrityComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java index 4d748b7d78f..e0f80ceb2a5 100644 --- a/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java +++ b/appcheck/firebase-appcheck-playintegrity/src/test/java/com/google/firebase/appcheck/playintegrity/internal/PlayIntegrityAppCheckProviderTest.java @@ -34,8 +34,9 @@ import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.concurrent.TestOnlyExecutors; import java.io.IOException; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import org.json.JSONObject; import org.junit.Before; @@ -70,7 +71,9 @@ public class PlayIntegrityAppCheckProviderTest { @Captor private ArgumentCaptor integrityTokenRequestCaptor; @Captor private ArgumentCaptor exchangePlayIntegrityTokenRequestCaptor; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). + private Executor liteExecutor = MoreExecutors.directExecutor(); + private Executor blockingExecutor = MoreExecutors.directExecutor(); @Before public void setup() { @@ -85,7 +88,8 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new PlayIntegrityAppCheckProvider(null); + new PlayIntegrityAppCheckProvider( + null, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking()); }); } @@ -104,7 +108,8 @@ public void getToken_onSuccess_setsTaskResult() throws Exception { PROJECT_NUMBER, mockIntegrityManager, mockNetworkClient, - backgroundExecutor, + liteExecutor, + blockingExecutor, mockRetryManager); Task task = provider.getToken(); @@ -139,7 +144,8 @@ public void getToken_generateChallengeFails_setsTaskException() throws Exception PROJECT_NUMBER, mockIntegrityManager, mockNetworkClient, - backgroundExecutor, + liteExecutor, + blockingExecutor, mockRetryManager); Task task = provider.getToken(); @@ -163,7 +169,8 @@ public void getToken_requestIntegrityTokenFails_setsTaskException() throws Excep PROJECT_NUMBER, mockIntegrityManager, mockNetworkClient, - backgroundExecutor, + liteExecutor, + blockingExecutor, mockRetryManager); Task task = provider.getToken(); @@ -194,7 +201,8 @@ public void getToken_tokenExchangeFails_setsTaskException() throws Exception { PROJECT_NUMBER, mockIntegrityManager, mockNetworkClient, - backgroundExecutor, + liteExecutor, + blockingExecutor, mockRetryManager); Task task = provider.getToken(); diff --git a/appcheck/firebase-appcheck-safetynet/CHANGELOG.md b/appcheck/firebase-appcheck-safetynet/CHANGELOG.md index 39e56b1e276..6537483a31e 100644 --- a/appcheck/firebase-appcheck-safetynet/CHANGELOG.md +++ b/appcheck/firebase-appcheck-safetynet/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. (#4431, #4449) +* [changed] Integrated the [app_check] SafetyNet SDK with Firebase Components. (#4436) +* [changed] Moved Task continuations off the main thread. (#4453) # 16.1.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle b/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle index f9b52b74e06..d4ef33efdad 100644 --- a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle +++ b/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle @@ -41,6 +41,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(':appcheck:firebase-appcheck') @@ -51,6 +52,7 @@ dependencies { javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' javadocClasspath 'org.checkerframework:checker-qual:2.5.2' + testImplementation project(':integ-testing') testImplementation 'junit:junit:4.13-beta-2' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation "org.robolectric:robolectric:$robolectricVersion" diff --git a/appcheck/firebase-appcheck-safetynet/gradle.properties b/appcheck/firebase-appcheck-safetynet/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck-safetynet/gradle.properties +++ b/appcheck/firebase-appcheck-safetynet/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrar.java b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrar.java index 9e7080fda7c..03700931817 100644 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrar.java +++ b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrar.java @@ -15,11 +15,19 @@ package com.google.firebase.appcheck.safetynet; import com.google.android.gms.common.annotation.KeepForSdk; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.appcheck.safetynet.internal.SafetyNetAppCheckProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * {@link ComponentRegistrar} for setting up FirebaseAppCheck safety net's dependency injections in @@ -33,6 +41,25 @@ public class FirebaseAppCheckSafetyNetRegistrar implements ComponentRegistrar { @Override public List> getComponents() { - return Arrays.asList(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(SafetyNetAppCheckProvider.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + (container) -> + new SafetyNetAppCheckProvider( + container.get(FirebaseApp.class), + container.get(liteExecutor), + container.get(backgroundExecutor), + container.get(blockingExecutor))) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } } diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java index 7ddc9889ce8..ec6515d7282 100644 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java +++ b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/SafetyNetAppCheckProviderFactory.java @@ -23,10 +23,7 @@ /** * Implementation of an {@link AppCheckProviderFactory} that builds {@link * SafetyNetAppCheckProvider}s. This is the default implementation. - * - * @deprecated Use {@code PlayIntegrityAppCheckProviderFactory} instead. */ -@Deprecated public class SafetyNetAppCheckProviderFactory implements AppCheckProviderFactory { private static final SafetyNetAppCheckProviderFactory instance = @@ -37,10 +34,7 @@ private SafetyNetAppCheckProviderFactory() {} /** * Gets an instance of this class for installation into a {@link * com.google.firebase.appcheck.FirebaseAppCheck} instance. - * - * @deprecated Use {@code PlayIntegrityAppCheckProviderFactory#getInstance} instead. */ - @Deprecated @NonNull public static SafetyNetAppCheckProviderFactory getInstance() { return instance; @@ -48,7 +42,8 @@ public static SafetyNetAppCheckProviderFactory getInstance() { @NonNull @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { - return new SafetyNetAppCheckProvider(firebaseApp); + return firebaseApp.get(SafetyNetAppCheckProvider.class); } } diff --git a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java index f44867c39ee..940bc91bab8 100644 --- a/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java +++ b/appcheck/firebase-appcheck-safetynet/src/main/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProvider.java @@ -25,19 +25,19 @@ import com.google.android.gms.safetynet.SafetyNet; import com.google.android.gms.safetynet.SafetyNetApi; import com.google.android.gms.safetynet.SafetyNetClient; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckToken; -import com.google.firebase.appcheck.internal.AppCheckTokenResponse; import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; public class SafetyNetAppCheckProvider implements AppCheckProvider { @@ -48,20 +48,26 @@ public class SafetyNetAppCheckProvider implements AppCheckProvider { private static final String NONCE = ""; private static final String UTF_8 = "UTF-8"; - private final Context context; private final Task safetyNetClientTask; private final NetworkClient networkClient; - private final ExecutorService backgroundExecutor; + private final Executor liteExecutor; + private final Executor blockingExecutor; private final RetryManager retryManager; private final String apiKey; /** @param firebaseApp the FirebaseApp to which this Factory is tied. */ - public SafetyNetAppCheckProvider(@NonNull FirebaseApp firebaseApp) { + public SafetyNetAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @Lightweight Executor liteExecutor, + @Background Executor backgroundExecutor, + @Blocking Executor blockingExecutor) { this( firebaseApp, new NetworkClient(firebaseApp), GoogleApiAvailability.getInstance(), - Executors.newCachedThreadPool()); + liteExecutor, + backgroundExecutor, + blockingExecutor); } @VisibleForTesting @@ -69,15 +75,19 @@ public SafetyNetAppCheckProvider(@NonNull FirebaseApp firebaseApp) { @NonNull FirebaseApp firebaseApp, @NonNull NetworkClient networkClient, @NonNull GoogleApiAvailability googleApiAvailability, - @NonNull ExecutorService backgroundExecutor) { + @NonNull Executor liteExecutor, + @NonNull Executor backgroundExecutor, + @NonNull Executor blockingExecutor) { checkNotNull(firebaseApp); checkNotNull(networkClient); checkNotNull(googleApiAvailability); checkNotNull(backgroundExecutor); - this.context = firebaseApp.getApplicationContext(); this.apiKey = firebaseApp.getOptions().getApiKey(); - this.backgroundExecutor = backgroundExecutor; - this.safetyNetClientTask = initSafetyNetClient(googleApiAvailability, this.backgroundExecutor); + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; + this.safetyNetClientTask = + initSafetyNetClient( + firebaseApp.getApplicationContext(), googleApiAvailability, backgroundExecutor); this.networkClient = networkClient; this.retryManager = new RetryManager(); } @@ -87,18 +97,19 @@ public SafetyNetAppCheckProvider(@NonNull FirebaseApp firebaseApp) { @NonNull FirebaseApp firebaseApp, @NonNull SafetyNetClient safetyNetClient, @NonNull NetworkClient networkClient, - @NonNull ExecutorService backgroundExecutor, + @NonNull Executor liteExecutor, + @NonNull Executor blockingExecutor, @NonNull RetryManager retryManager) { - this.context = firebaseApp.getApplicationContext(); this.apiKey = firebaseApp.getOptions().getApiKey(); this.safetyNetClientTask = Tasks.forResult(safetyNetClient); this.networkClient = networkClient; - this.backgroundExecutor = backgroundExecutor; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; this.retryManager = retryManager; } - private Task initSafetyNetClient( - GoogleApiAvailability googleApiAvailability, ExecutorService executor) { + private static Task initSafetyNetClient( + Context context, GoogleApiAvailability googleApiAvailability, Executor executor) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); executor.execute( () -> { @@ -115,7 +126,7 @@ private Task initSafetyNetClient( return taskCompletionSource.getTask(); } - private String getGooglePlayServicesConnectionErrorString(int connectionResult) { + private static String getGooglePlayServicesConnectionErrorString(int connectionResult) { switch (connectionResult) { case ConnectionResult.SERVICE_MISSING: return "Google Play services is missing on this device."; @@ -141,33 +152,9 @@ Task getSafetyNetClientTask() { @Override public Task getToken() { return safetyNetClientTask - .continueWithTask( - new Continuation>() { - @Override - public Task then( - @NonNull Task task) { - if (task.isSuccessful()) { - return task.getResult().attest(NONCE.getBytes(), apiKey); - } - return Tasks.forException(task.getException()); - } - }) - .continueWithTask( - new Continuation>() { - @Override - public Task then( - @NonNull Task task) { - if (!task.isSuccessful()) { - // Proxies errors to the client directly; need to wrap to get the - // types right. - // TODO: more specific error mapping to help clients debug more - // easily. - return Tasks.forException(task.getException()); - } else { - return exchangeSafetyNetAttestationResponseForToken(task.getResult()); - } - } - }); + .onSuccessTask( + liteExecutor, safetyNetClient -> safetyNetClient.attest(NONCE.getBytes(), apiKey)) + .onSuccessTask(liteExecutor, this::exchangeSafetyNetAttestationResponseForToken); } @NonNull @@ -179,25 +166,14 @@ Task exchangeSafetyNetAttestationResponseForToken( ExchangeSafetyNetTokenRequest request = new ExchangeSafetyNetTokenRequest(safetyNetJwsResult); - Task networkTask = - Tasks.call( - backgroundExecutor, + return Tasks.call( + blockingExecutor, () -> networkClient.exchangeAttestationForAppCheckToken( - request.toJsonString().getBytes(UTF_8), - NetworkClient.SAFETY_NET, - retryManager)); - return networkTask.continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckToken.constructFromAppCheckTokenResponse(task.getResult())); - } - // TODO: Surface more error details. - return Tasks.forException(task.getException()); - } - }); + request.toJsonString().getBytes(UTF_8), NetworkClient.SAFETY_NET, retryManager)) + .onSuccessTask( + liteExecutor, + response -> + Tasks.forResult(DefaultAppCheckToken.constructFromAppCheckTokenResponse(response))); } } diff --git a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrarTest.java b/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrarTest.java new file mode 100644 index 00000000000..9dae114da0f --- /dev/null +++ b/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/FirebaseAppCheckSafetyNetRegistrarTest.java @@ -0,0 +1,50 @@ +// Copyright 2022 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.appcheck.safetynet; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.components.Component; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FirebaseAppCheckSafetyNetRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckSafetyNetRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckSafetyNetRegistrar registrar = new FirebaseAppCheckSafetyNetRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component appCheckSafetyNetComponent = components.get(0); + assertThat(appCheckSafetyNetComponent.getDependencies()) + .containsExactly( + Dependency.required(FirebaseApp.class), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Background.class, Executor.class)), + Dependency.required(Qualified.qualified(Blocking.class, Executor.class))); + assertThat(appCheckSafetyNetComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java b/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java index 6072e3693f1..fed93464c2f 100644 --- a/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java +++ b/appcheck/firebase-appcheck-safetynet/src/test/java/com/google/firebase/appcheck/safetynet/internal/SafetyNetAppCheckProviderTest.java @@ -37,8 +37,8 @@ import com.google.firebase.appcheck.internal.DefaultAppCheckToken; import com.google.firebase.appcheck.internal.NetworkClient; import com.google.firebase.appcheck.internal.RetryManager; +import com.google.firebase.concurrent.TestOnlyExecutors; import java.io.IOException; -import java.util.concurrent.ExecutorService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,7 +62,6 @@ public class SafetyNetAppCheckProviderTest { private static final String TIME_TO_LIVE = "3600s"; private FirebaseApp firebaseApp; - private ExecutorService backgroundExecutor = MoreExecutors.newDirectExecutorService(); @Mock private GoogleApiAvailability mockGoogleApiAvailability; @Mock private SafetyNetClient mockSafetyNetClient; @Mock private NetworkClient mockNetworkClient; @@ -84,7 +83,11 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new SafetyNetAppCheckProvider(null); + new SafetyNetAppCheckProvider( + null, + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -95,7 +98,12 @@ public void testPublicConstructor_nullFirebaseApp_expectThrows() { .thenReturn(ConnectionResult.SERVICE_MISSING); SafetyNetAppCheckProvider provider = new SafetyNetAppCheckProvider( - firebaseApp, mockNetworkClient, mockGoogleApiAvailability, backgroundExecutor); + firebaseApp, + mockNetworkClient, + mockGoogleApiAvailability, + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); assertThat(provider.getSafetyNetClientTask().isSuccessful()).isFalse(); } @@ -105,7 +113,12 @@ public void testGetToken_googlePlayServicesIsNotAvailable_expectGetTokenTaskExce .thenReturn(ConnectionResult.SERVICE_MISSING); SafetyNetAppCheckProvider provider = new SafetyNetAppCheckProvider( - firebaseApp, mockNetworkClient, mockGoogleApiAvailability, backgroundExecutor); + firebaseApp, + mockNetworkClient, + mockGoogleApiAvailability, + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); assertThat(provider.getSafetyNetClientTask().isSuccessful()).isFalse(); Task tokenTask = provider.getToken(); @@ -115,12 +128,14 @@ public void testGetToken_googlePlayServicesIsNotAvailable_expectGetTokenTaskExce @Test public void testGetToken_nonNullSafetyNetClient_expectCallsSafetyNetForAttestation() { + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). SafetyNetAppCheckProvider provider = new SafetyNetAppCheckProvider( firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + /* liteExecutor= */ MoreExecutors.directExecutor(), + TestOnlyExecutors.blocking(), mockRetryManager); assertThat(provider.getSafetyNetClientTask().getResult()).isEqualTo(mockSafetyNetClient); @@ -142,7 +157,8 @@ public void testExchangeSafetyNetJwsForToken_nullAttestationResponse_expectThrow firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + TestOnlyExecutors.lite(), + TestOnlyExecutors.blocking(), mockRetryManager); assertThrows( NullPointerException.class, @@ -160,7 +176,8 @@ public void testExchangeSafetyNetJwsForToken_emptySafetyNetJwsResult_expectThrow firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + TestOnlyExecutors.lite(), + TestOnlyExecutors.blocking(), mockRetryManager); assertThrows( IllegalArgumentException.class, @@ -177,7 +194,8 @@ public void testExchangeSafetyNetJwsForToken_validFields_expectReturnsTask() { firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + TestOnlyExecutors.lite(), + TestOnlyExecutors.blocking(), mockRetryManager); Task task = provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); @@ -193,12 +211,14 @@ public void exchangeSafetyNetJwsForToken_onSuccess_setsTaskResult() throws Excep when(mockAppCheckTokenResponse.getToken()).thenReturn(APP_CHECK_TOKEN); when(mockAppCheckTokenResponse.getTimeToLive()).thenReturn(TIME_TO_LIVE); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). SafetyNetAppCheckProvider provider = new SafetyNetAppCheckProvider( firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + /* liteExecutor= */ MoreExecutors.directExecutor(), + /* blockingExecutor= */ MoreExecutors.directExecutor(), mockRetryManager); Task task = provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); @@ -219,12 +239,14 @@ public void exchangeSafetyNetJwsForToken_onFailure_setsTaskException() throws Ex any(), eq(NetworkClient.SAFETY_NET), eq(mockRetryManager))) .thenThrow(new IOException()); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). SafetyNetAppCheckProvider provider = new SafetyNetAppCheckProvider( firebaseApp, mockSafetyNetClient, mockNetworkClient, - backgroundExecutor, + /* liteExecutor= */ MoreExecutors.directExecutor(), + /* blockingExecutor= */ MoreExecutors.directExecutor(), mockRetryManager); Task task = provider.exchangeSafetyNetAttestationResponseForToken(mockSafetyNetAttestationResponse); diff --git a/appcheck/firebase-appcheck/CHANGELOG.md b/appcheck/firebase-appcheck/CHANGELOG.md index e66859e9abe..c84692d0e50 100644 --- a/appcheck/firebase-appcheck/CHANGELOG.md +++ b/appcheck/firebase-appcheck/CHANGELOG.md @@ -1,4 +1,6 @@ # Unreleased +* [changed] Migrated [app_check] SDKs to use standard Firebase executors. (#4431, #4449) +* [changed] Moved Task continuations off the main thread. (#4453) # 16.1.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/appcheck/firebase-appcheck/firebase-appcheck.gradle b/appcheck/firebase-appcheck/firebase-appcheck.gradle index 6ed2202dd30..d1e821ca2a9 100644 --- a/appcheck/firebase-appcheck/firebase-appcheck.gradle +++ b/appcheck/firebase-appcheck/firebase-appcheck.gradle @@ -29,6 +29,7 @@ android { defaultConfig { targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion + multiDexEnabled = true versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -41,6 +42,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(":appcheck:firebase-appcheck-interop") @@ -49,6 +51,7 @@ dependencies { javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + testImplementation project(':integ-testing') testImplementation 'junit:junit:4.13-beta-2' testImplementation 'org.mockito:mockito-core:2.25.0' testImplementation 'org.mockito:mockito-inline:2.25.0' diff --git a/appcheck/firebase-appcheck/gradle.properties b/appcheck/firebase-appcheck/gradle.properties index 0e34974b3c7..9fcdf80f72a 100644 --- a/appcheck/firebase-appcheck/gradle.properties +++ b/appcheck/firebase-appcheck/gradle.properties @@ -1,2 +1,2 @@ -version=16.0.3 -latestReleasedVersion=16.0.2 +version=16.1.1 +latestReleasedVersion=16.1.0 diff --git a/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt b/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt index 7364516fe9d..f71c2d55bd1 100644 --- a/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt +++ b/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt @@ -37,70 +37,70 @@ const val EXISTING_APP = "existing" @RunWith(AndroidJUnit4ClassRunner::class) abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(AndroidJUnit4ClassRunner::class) class FirebaseAppCheckTests : BaseTestCase() { - @Test - fun appCheck_default_callsDefaultGetInstance() { - assertThat(Firebase.appCheck).isSameInstanceAs(FirebaseAppCheck.getInstance()) - } + @Test + fun appCheck_default_callsDefaultGetInstance() { + assertThat(Firebase.appCheck).isSameInstanceAs(FirebaseAppCheck.getInstance()) + } - @Test - fun appCheck_with_custom_firebaseapp_calls_GetInstance() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.appCheck(app)) - .isSameInstanceAs(FirebaseAppCheck.getInstance(app)) - } + @Test + fun appCheck_with_custom_firebaseapp_calls_GetInstance() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.appCheck(app)).isSameInstanceAs(FirebaseAppCheck.getInstance(app)) + } - @Test - fun appCheckToken_destructuring_declaration_works() { - val mockAppCheckToken = object : AppCheckToken() { - override fun getToken(): String = "randomToken" + @Test + fun appCheckToken_destructuring_declaration_works() { + val mockAppCheckToken = + object : AppCheckToken() { + override fun getToken(): String = "randomToken" - override fun getExpireTimeMillis(): Long = 23121997 - } + override fun getExpireTimeMillis(): Long = 23121997 + } - val (token, expiration) = mockAppCheckToken + val (token, expiration) = mockAppCheckToken - assertThat(token).isEqualTo(mockAppCheckToken.token) - assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) - } + assertThat(token).isEqualTo(mockAppCheckToken.token) + assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) + } } internal const val LIBRARY_NAME: String = "fire-app-check-ktx" @RunWith(AndroidJUnit4ClassRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun libraryRegistrationAtRuntime() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt b/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt index 15e313d6f41..357f547f6a7 100644 --- a/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt +++ b/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt @@ -25,7 +25,7 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseAppCheck] instance of the default [FirebaseApp]. */ val Firebase.appCheck: FirebaseAppCheck - get() = FirebaseAppCheck.getInstance() + get() = FirebaseAppCheck.getInstance() /** Returns the [FirebaseAppCheck] instance of a given [FirebaseApp]. */ fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) @@ -48,6 +48,6 @@ internal const val LIBRARY_NAME: String = "fire-app-check-ktx" /** @suppress */ class FirebaseAppCheckKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java index 8a52acdf20a..b9ad885b77b 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrar.java @@ -16,16 +16,23 @@ import com.google.android.gms.common.annotation.KeepForSdk; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.appcheck.internal.DefaultFirebaseAppCheck; import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.heartbeatinfo.HeartBeatConsumerComponent; import com.google.firebase.heartbeatinfo.HeartBeatController; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; /** * {@link ComponentRegistrar} for setting up FirebaseAppCheck's dependency injections in Firebase @@ -39,16 +46,30 @@ public class FirebaseAppCheckRegistrar implements ComponentRegistrar { @Override public List> getComponents() { + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingScheduledExecutorService = + Qualified.qualified(Blocking.class, ScheduledExecutorService.class); + return Arrays.asList( Component.builder(FirebaseAppCheck.class, (InternalAppCheckTokenProvider.class)) .name(LIBRARY_NAME) .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(uiExecutor)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingScheduledExecutorService)) .add(Dependency.optionalProvider(HeartBeatController.class)) .factory( (container) -> new DefaultFirebaseAppCheck( container.get(FirebaseApp.class), - container.getProvider(HeartBeatController.class))) + container.getProvider(HeartBeatController.class), + container.get(uiExecutor), + container.get(liteExecutor), + container.get(backgroundExecutor), + container.get(blockingScheduledExecutorService))) .alwaysEager() .build(), HeartBeatConsumerComponent.create(), diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java index 769bd1048d3..1908bd1ad96 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java @@ -19,12 +19,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.appcheck.AppCheckProvider; import com.google.firebase.appcheck.AppCheckProviderFactory; import com.google.firebase.appcheck.AppCheckToken; @@ -36,8 +39,8 @@ import com.google.firebase.inject.Provider; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; public class DefaultFirebaseAppCheck extends FirebaseAppCheck { @@ -49,7 +52,9 @@ public class DefaultFirebaseAppCheck extends FirebaseAppCheck { private final List appCheckListenerList; private final StorageHelper storageHelper; private final TokenRefreshManager tokenRefreshManager; - private final ExecutorService backgroundExecutor; + private final Executor uiExecutor; + private final Executor liteExecutor; + private final Executor backgroundExecutor; private final Task retrieveStoredTokenTask; private final Clock clock; @@ -58,19 +63,12 @@ public class DefaultFirebaseAppCheck extends FirebaseAppCheck { private AppCheckToken cachedToken; public DefaultFirebaseAppCheck( - @NonNull FirebaseApp firebaseApp, - @NonNull Provider heartBeatController) { - this( - checkNotNull(firebaseApp), - checkNotNull(heartBeatController), - Executors.newCachedThreadPool()); - } - - @VisibleForTesting - DefaultFirebaseAppCheck( @NonNull FirebaseApp firebaseApp, @NonNull Provider heartBeatController, - @NonNull ExecutorService backgroundExecutor) { + @UiThread Executor uiExecutor, + @Lightweight Executor liteExecutor, + @Background Executor backgroundExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { checkNotNull(firebaseApp); checkNotNull(heartBeatController); this.firebaseApp = firebaseApp; @@ -80,13 +78,19 @@ public DefaultFirebaseAppCheck( this.storageHelper = new StorageHelper(firebaseApp.getApplicationContext(), firebaseApp.getPersistenceKey()); this.tokenRefreshManager = - new TokenRefreshManager(firebaseApp.getApplicationContext(), /* firebaseAppCheck= */ this); + new TokenRefreshManager( + firebaseApp.getApplicationContext(), + /* firebaseAppCheck= */ this, + liteExecutor, + scheduledExecutorService); + this.uiExecutor = uiExecutor; + this.liteExecutor = liteExecutor; this.backgroundExecutor = backgroundExecutor; this.retrieveStoredTokenTask = retrieveStoredAppCheckTokenInBackground(backgroundExecutor); this.clock = new Clock.DefaultClock(); } - private Task retrieveStoredAppCheckTokenInBackground(@NonNull ExecutorService executor) { + private Task retrieveStoredAppCheckTokenInBackground(@NonNull Executor executor) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); executor.execute( () -> { @@ -177,6 +181,7 @@ public void removeAppCheckListener(@NonNull AppCheckListener listener) { @Override public Task getToken(boolean forceRefresh) { return retrieveStoredTokenTask.continueWithTask( + liteExecutor, unused -> { if (!forceRefresh && hasValidToken()) { return Tasks.forResult( @@ -190,6 +195,7 @@ public Task getToken(boolean forceRefresh) { // TODO: Cache the in-flight task. return fetchTokenFromProvider() .continueWithTask( + liteExecutor, appCheckTokenTask -> { if (appCheckTokenTask.isSuccessful()) { return Tasks.forResult( @@ -211,6 +217,7 @@ public Task getToken(boolean forceRefresh) { @Override public Task getAppCheckToken(boolean forceRefresh) { return retrieveStoredTokenTask.continueWithTask( + liteExecutor, unused -> { if (!forceRefresh && hasValidToken()) { return Tasks.forResult(cachedToken); @@ -226,24 +233,19 @@ public Task getAppCheckToken(boolean forceRefresh) { Task fetchTokenFromProvider() { return appCheckProvider .getToken() - .continueWithTask( - new Continuation>() { - @Override - public Task then(@NonNull Task task) { - if (task.isSuccessful()) { - AppCheckToken token = task.getResult(); - updateStoredToken(token); - for (AppCheckListener listener : appCheckListenerList) { - listener.onAppCheckTokenChanged(token); - } - AppCheckTokenResult tokenResult = - DefaultAppCheckTokenResult.constructFromAppCheckToken(token); - for (AppCheckTokenListener listener : appCheckTokenListenerList) { - listener.onAppCheckTokenChanged(tokenResult); - } - } - return task; + .onSuccessTask( + uiExecutor, + token -> { + updateStoredToken(token); + for (AppCheckListener listener : appCheckListenerList) { + listener.onAppCheckTokenChanged(token); + } + AppCheckTokenResult tokenResult = + DefaultAppCheckTokenResult.constructFromAppCheckToken(token); + for (AppCheckTokenListener listener : appCheckTokenListenerList) { + listener.onAppCheckTokenChanged(tokenResult); } + return Tasks.forResult(token); }); } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java index 8e7f6722046..5098b8a6d53 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultTokenRefresher.java @@ -20,8 +20,9 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.OnFailureListener; -import java.util.concurrent.Executors; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -36,19 +37,18 @@ public class DefaultTokenRefresher { @VisibleForTesting static final long MAX_DELAY_SECONDS = 16 * 60; // 16 minutes private final DefaultFirebaseAppCheck firebaseAppCheck; + private final Executor liteExecutor; private final ScheduledExecutorService scheduledExecutorService; private volatile ScheduledFuture refreshFuture; private volatile long delayAfterFailureSeconds; - DefaultTokenRefresher(@NonNull DefaultFirebaseAppCheck firebaseAppCheck) { - this(checkNotNull(firebaseAppCheck), Executors.newScheduledThreadPool(/* corePoolSize= */ 1)); - } - - @VisibleForTesting DefaultTokenRefresher( - DefaultFirebaseAppCheck firebaseAppCheck, ScheduledExecutorService scheduledExecutorService) { - this.firebaseAppCheck = firebaseAppCheck; + @NonNull DefaultFirebaseAppCheck firebaseAppCheck, + @Lightweight Executor liteExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { + this.firebaseAppCheck = checkNotNull(firebaseAppCheck); + this.liteExecutor = liteExecutor; this.scheduledExecutorService = scheduledExecutorService; this.delayAfterFailureSeconds = UNSET_DELAY; } @@ -93,13 +93,7 @@ private long getNextRefreshMillis() { private void onRefresh() { firebaseAppCheck .fetchTokenFromProvider() - .addOnFailureListener( - new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - scheduleRefreshAfterFailure(); - } - }); + .addOnFailureListener(liteExecutor, e -> scheduleRefreshAfterFailure()); } /** Cancels the in-flight scheduled refresh. */ diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java index ac116dd0ca7..4ac16a05c5b 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/TokenRefreshManager.java @@ -22,8 +22,12 @@ import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.api.internal.BackgroundDetector; import com.google.android.gms.common.api.internal.BackgroundDetector.BackgroundStateChangeListener; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.AppCheckToken; import com.google.firebase.appcheck.internal.util.Clock; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; /** Class to manage whether or not to schedule an {@link AppCheckToken} refresh attempt. */ public final class TokenRefreshManager { @@ -41,10 +45,15 @@ public final class TokenRefreshManager { private volatile long nextRefreshTimeMillis; private volatile boolean isAutoRefreshEnabled; - TokenRefreshManager(@NonNull Context context, @NonNull DefaultFirebaseAppCheck firebaseAppCheck) { + TokenRefreshManager( + @NonNull Context context, + @NonNull DefaultFirebaseAppCheck firebaseAppCheck, + @Lightweight Executor liteExecutor, + @Blocking ScheduledExecutorService scheduledExecutorService) { this( checkNotNull(context), - new DefaultTokenRefresher(checkNotNull(firebaseAppCheck)), + new DefaultTokenRefresher( + checkNotNull(firebaseAppCheck), liteExecutor, scheduledExecutorService), new Clock.DefaultClock()); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java index c6a39d3a45e..c5e6a35d112 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/FirebaseAppCheckRegistrarTest.java @@ -17,10 +17,17 @@ import static com.google.common.truth.Truth.assertThat; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.heartbeatinfo.HeartBeatController; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -39,6 +46,11 @@ public void testGetComponents() { assertThat(firebaseAppCheckComponent.getDependencies()) .containsExactly( Dependency.required(FirebaseApp.class), + Dependency.required(Qualified.qualified(UiThread.class, Executor.class)), + Dependency.required(Qualified.qualified(Lightweight.class, Executor.class)), + Dependency.required(Qualified.qualified(Background.class, Executor.class)), + Dependency.required( + Qualified.qualified(Blocking.class, ScheduledExecutorService.class)), Dependency.optionalProvider(HeartBeatController.class)); assertThat(firebaseAppCheckComponent.isAlwaysEager()).isTrue(); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java index 7b065e4a827..48f881ec658 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheckTest.java @@ -32,6 +32,7 @@ import com.google.firebase.appcheck.AppCheckTokenResult; import com.google.firebase.appcheck.FirebaseAppCheck.AppCheckListener; import com.google.firebase.appcheck.interop.AppCheckTokenListener; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.heartbeatinfo.HeartBeatController; import org.junit.Before; import org.junit.Test; @@ -76,11 +77,15 @@ public void setup() { when(mockAppCheckProviderFactory.create(any())).thenReturn(mockAppCheckProvider); when(mockAppCheckProvider.getToken()).thenReturn(Tasks.forResult(validDefaultAppCheckToken)); + // TODO(b/258273630): Use TestOnlyExecutors instead of MoreExecutors.directExecutor(). defaultFirebaseAppCheck = new DefaultFirebaseAppCheck( mockFirebaseApp, () -> mockHeartBeatController, - MoreExecutors.newDirectExecutorService()); + TestOnlyExecutors.ui(), + /* liteExecutor= */ MoreExecutors.directExecutor(), + /* backgroundExecutor= */ MoreExecutors.directExecutor(), + TestOnlyExecutors.blocking()); } @Test @@ -88,7 +93,13 @@ public void testConstructor_nullFirebaseApp_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DefaultFirebaseAppCheck(null, () -> mockHeartBeatController); + new DefaultFirebaseAppCheck( + null, + () -> mockHeartBeatController, + TestOnlyExecutors.ui(), + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } @@ -97,7 +108,13 @@ public void testConstructor_nullHeartBeatControllerProvider_expectThrows() { assertThrows( NullPointerException.class, () -> { - new DefaultFirebaseAppCheck(mockFirebaseApp, null); + new DefaultFirebaseAppCheck( + mockFirebaseApp, + null, + TestOnlyExecutors.ui(), + TestOnlyExecutors.lite(), + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking()); }); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java index 44805aca22b..0a3f31ddb3f 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/DefaultTokenRefresherTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import com.google.android.gms.tasks.Tasks; +import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.appcheck.AppCheckToken; import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; @@ -44,7 +45,6 @@ public class DefaultTokenRefresherTest { private static final long TWO_MINUTES_SECONDS = 2L * 60L; private static final long FOUR_MINUTES_SECONDS = 4L * 60L; private static final long EIGHT_MINUTES_SECONDS = 8L * 60L; - private static final String ERROR = "error"; @Mock DefaultFirebaseAppCheck mockFirebaseAppCheck; @Mock ScheduledExecutorService mockScheduledExecutorService; @@ -59,8 +59,10 @@ public void setUp() { when(mockFirebaseAppCheck.fetchTokenFromProvider()) .thenReturn(Tasks.forResult(mockAppCheckToken)); + // TODO(b/258273630): Use TestOnlyExecutors. defaultTokenRefresher = - new DefaultTokenRefresher(mockFirebaseAppCheck, mockScheduledExecutorService); + new DefaultTokenRefresher( + mockFirebaseAppCheck, MoreExecutors.directExecutor(), mockScheduledExecutorService); } @Test diff --git a/build.gradle b/build.gradle index c52b79c8f04..27caf4dd2f3 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,9 @@ import com.google.firebase.gradle.plugins.license.LicenseResolverPlugin import com.google.firebase.gradle.MultiProjectReleasePlugin buildscript { - ext.kotlinVersion = '1.7.10' - ext.coroutinesVersion = '1.6.4' + // TODO: remove once all sdks have migrated to version catalog + ext.kotlinVersion = libs.versions.kotlin.get() + ext.coroutinesVersion = libs.versions.coroutines.get() repositories { google() @@ -39,7 +40,7 @@ buildscript { classpath 'net.ltgt.gradle:gradle-errorprone-plugin:1.3.0' classpath 'gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.9' classpath 'com.google.gms:google-services:4.3.3' - classpath 'org.jlleitschuh.gradle:ktlint-gradle:9.2.1' + classpath "com.ncorti.ktfmt.gradle:plugin:0.11.0" } } @@ -47,13 +48,12 @@ apply from: 'sdkProperties.gradle' apply from: "gradle/errorProne.gradle" ext { - playServicesVersion = '16.0.1' - supportAnnotationsVersion = '28.0.0' - googleTruthVersion = '1.1.2' - grpcVersion = '1.48.1' - robolectricVersion = '4.9' - protocVersion = '3.17.3' - javaliteVersion = '3.17.3' + // TODO: remove once all sdks have migrated to version catalog + googleTruthVersion = libs.versions.truth.get() + grpcVersion = libs.versions.grpc.get() + robolectricVersion = libs.versions.robolectric.get() + protocVersion = libs.versions.protoc.get() + javaliteVersion = libs.versions.javalite.get() } apply plugin: com.google.firebase.gradle.plugins.publish.PublishingPlugin @@ -66,6 +66,7 @@ firebaseContinuousIntegration { ignorePaths = [ /.*\.gitignore$/, /.*\/.*.md$/, + /.*\.github.*/, ] } @@ -86,7 +87,11 @@ configure(subprojects) { } } - apply plugin: "org.jlleitschuh.gradle.ktlint" + apply plugin: "com.ncorti.ktfmt.gradle" + + ktfmt { + googleStyle() + } } task clean(type: Delete) { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 307a9266d52..470579f53b6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -13,7 +13,7 @@ // limitations under the License. plugins { - id("org.jlleitschuh.gradle.ktlint") version "9.2.1" + id("com.ncorti.ktfmt.gradle") version "0.11.0" id("com.github.sherter.google-java-format") version "0.9" `kotlin-dsl` } @@ -32,7 +32,11 @@ repositories { val perfPluginVersion = System.getenv("FIREBASE_PERF_PLUGIN_VERSION") ?: "1.4.1" googleJavaFormat { - toolVersion = "1.10.0" + toolVersion = "1.15.0" +} + +ktfmt { + googleStyle() } dependencies { @@ -102,3 +106,9 @@ tasks.withType { val enablePluginTests: String? by rootProject enabled = enablePluginTests == "true" } + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = "11" + } +} diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/MultiProjectReleasePlugin.java b/buildSrc/src/main/java/com/google/firebase/gradle/MultiProjectReleasePlugin.java index 03ac99f965b..35e976a809a 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/MultiProjectReleasePlugin.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/MultiProjectReleasePlugin.java @@ -15,24 +15,19 @@ import static com.google.firebase.gradle.plugins.ProjectUtilsKt.toBoolean; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.gradle.bomgenerator.BomGeneratorTask; import com.google.firebase.gradle.plugins.FireEscapeArtifactPlugin; import com.google.firebase.gradle.plugins.FirebaseLibraryExtension; import com.google.firebase.gradle.plugins.JavadocPlugin; -import com.google.firebase.gradle.plugins.TasksKt; import com.google.firebase.gradle.plugins.publish.PublishingPlugin; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; import java.util.Set; import java.util.stream.Collectors; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; -import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.bundling.Zip; /** @@ -62,10 +57,7 @@ public void apply(Project project) { project.apply(ImmutableMap.of("plugin", PublishingPlugin.class)); boolean releaseJavadocs = toBoolean(System.getProperty("releaseJavadocs", "true")); - - File firebaseDevsiteJavadoc = new File(project.getBuildDir(), "firebase-javadocs/"); - File firebaseClientBuildDest = new File(firebaseDevsiteJavadoc, "client/"); - firebaseClientBuildDest.mkdirs(); + File firebaseDevsiteJavadoc = new File(project.getBuildDir(), "firebase-kotlindoc/android"); project.subprojects( sub -> { @@ -118,108 +110,10 @@ public void apply(Project project) { "generateAllJavadocs", task -> { for (Project p : projectsToPublish) { - task.dependsOn(p.getPath() + ":" + TasksKt.JAVADOC_TASK_NAME); - - task.doLast( - t -> { - for (Project publishableProject : projectsToPublish) { - publishableProject.copy( - copy -> { - copy.from( - publishableProject.getBuildDir() - + "/docs/javadoc/reference"); - copy.include("**/*"); - copy.into(firebaseDevsiteJavadoc); - }); - - publishableProject.copy( - copy -> { - copy.from( - publishableProject.getBuildDir() - + "/docs/javadoc/reference/_toc.yaml"); - copy.include("**/*"); - copy.into( - firebaseClientBuildDest - + "/" - + publishableProject.getName()); - }); - } - }); + task.dependsOn(p.getPath() + ":kotlindoc"); } }); - Delete prepareJavadocs = - project - .getTasks() - .create( - "prepareJavadocs", - Delete.class, - del -> { - del.dependsOn(generateAllJavadocs); - del.doLast( - d -> { - // cleanup docs - project.delete( - delSpec -> { - ImmutableList relativeDeletablePaths = - ImmutableList.of( - "timestamp.js", - "navtree_data.js", - "assets/", - "classes.html", - "hierarchy.html", - "lists.js", - "package-list", - "packages.html", - "index.html", - "current.xml", - "_toc.yaml"); - delSpec.delete( - relativeDeletablePaths.stream() - .map( - path -> - firebaseDevsiteJavadoc.getPath() - + "/" - + path) - .collect(Collectors.toList())); - }); - // Transform - project.exec( - execSpec -> { - execSpec.setIgnoreExitValue(true); - execSpec.setWorkingDir(firebaseDevsiteJavadoc); - execSpec.setCommandLine( - project.getRootProject().file("buildSrc").getPath() - + "/firesite_transform.sh"); - }); - - // Tidy - String tidyBinary = System.getProperty("tidyBinaryPath", null); - String tidyConfig = System.getProperty("tidyConfigPath", null); - if (tidyBinary != null && tidyConfig != null) { - try { - Files.walk(firebaseDevsiteJavadoc.toPath()) - .filter( - p -> - p.toFile().isFile() - && p.toString().endsWith(".html")) - .forEach( - p -> { - project.exec( - execSpec -> { - System.out.println("Tidying " + p); - execSpec.setIgnoreExitValue(true); - execSpec.commandLine( - tidyBinary, "-config", tidyConfig, p); - }); - }); - } catch (IOException e) { - throw new GradleException("Directory walk failed.", e); - } - } - }); - }); - Zip assembleFirebaseJavadocZip = project .getTasks() @@ -227,7 +121,7 @@ public void apply(Project project) { "assembleFirebaseJavadocZip", Zip.class, zip -> { - zip.dependsOn(prepareJavadocs); + zip.dependsOn(generateAllJavadocs); zip.getDestinationDirectory().set(project.getBuildDir()); zip.getArchiveFileName().set("firebase-javadoc.zip"); zip.from(firebaseDevsiteJavadoc); diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/NdkBinaryFixTask.kt b/buildSrc/src/main/java/com/google/firebase/gradle/NdkBinaryFixTask.kt new file mode 100644 index 00000000000..303f734059c --- /dev/null +++ b/buildSrc/src/main/java/com/google/firebase/gradle/NdkBinaryFixTask.kt @@ -0,0 +1,45 @@ +// Copyright 2021 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 + +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class NdkBinaryFixTask : DefaultTask() { + @get:InputFile abstract val inputFile: RegularFileProperty + + @get:OutputFile + val outputFile: File + get() = inputFile.get().asFile.let { File(it.parentFile, "lib${it.name}.so") } + + @get:Internal + val into: String + get() = "jni/${outputFile.parentFile.name}" + + @TaskAction + fun run() { + Files.copy( + inputFile.get().asFile.toPath(), + outputFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } +} diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaGenerationTask.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaGenerationTask.kt index be77a4ef3aa..13d0cd2b1b0 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaGenerationTask.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaGenerationTask.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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 import java.io.File @@ -21,6 +35,7 @@ import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import org.gradle.workers.WorkerExecutor import org.json.JSONObject + /** * Extension class for [GenerateDocumentationTask]. * @@ -34,123 +49,128 @@ import org.json.JSONObject */ @CacheableTask abstract class GenerateDocumentationTaskExtension : DefaultTask() { - @get:[InputFile Classpath] - abstract val dackkaJarFile: Property + @get:[InputFile Classpath] + abstract val dackkaJarFile: Property - @get:[InputFiles Classpath] - abstract val dependencies: Property + @get:[InputFiles Classpath] + abstract val dependencies: Property - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val sources: ListProperty + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sources: ListProperty - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val suppressedFiles: ListProperty + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val suppressedFiles: ListProperty - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val packageListFiles: ListProperty + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val packageListFiles: ListProperty - @get:Input - abstract val clientName: Property + @get:Input abstract val clientName: Property - @get:OutputDirectory - abstract val outputDirectory: Property + @get:OutputDirectory abstract val outputDirectory: Property } /** * Wrapper data class for External package-lists in Dokka * - * This class allows us to map package-lists in a type-safe way, versus inline straight to - * a map. This extra step could be removed- but it could also catch bugs in the future. + * This class allows us to map package-lists in a type-safe way, versus inline straight to a map. + * This extra step could be removed- but it could also catch bugs in the future. * * @property packageList the prepared package-list file to map against * @property externalLink the url to map with when generating the docs */ -data class ExternalDocumentationLink( - val packageList: File, - val externalLink: String -) +data class ExternalDocumentationLink(val packageList: File, val externalLink: String) /** * Task to run Dackka on a project. * - * Since dackka needs to be run on the command line, we have to organize the arguments for dackka into - * a json file. We then pass that json file to dackka as an argument. + * Since dackka needs to be run on the command line, we have to organize the arguments for dackka + * into a json file. We then pass that json file to dackka as an argument. * * @see GenerateDocumentationTaskExtension */ -abstract class GenerateDocumentationTask @Inject constructor( - private val workerExecutor: WorkerExecutor -) : GenerateDocumentationTaskExtension() { - - @TaskAction - fun build() { - val configFile = saveToJsonFile(constructArguments()) - launchDackka(clientName, configFile, workerExecutor) +abstract class GenerateDocumentationTask +@Inject +constructor(private val workerExecutor: WorkerExecutor) : GenerateDocumentationTaskExtension() { + + @TaskAction + fun build() { + val configFile = saveToJsonFile(constructArguments()) + launchDackka(clientName, configFile, workerExecutor) + } + + private fun constructArguments(): JSONObject { + val jsonMap = + mapOf( + "moduleName" to "", + "outputDir" to outputDirectory.get().path, + "globalLinks" to "", + "sourceSets" to + listOf( + mutableMapOf( + "sourceSetID" to mapOf("scopeId" to "androidx", "sourceSetName" to "main"), + "sourceRoots" to sources.get().map { it.path }, + "classpath" to dependencies.get().map { it.path }, + "documentedVisibilities" to listOf("PUBLIC", "PROTECTED"), + "skipEmptyPackages" to "true", + "suppressedFiles" to suppressedFiles.get().map { it.path }, + "externalDocumentationLinks" to + createExternalLinks(packageListFiles).map { + mapOf("url" to it.externalLink, "packageListUrl" to it.packageList.toURI()) + } + ) + ), + "offlineMode" to "true", + "noJdkLink" to "true" + ) + + return JSONObject(jsonMap) + } + + private fun createExternalLinks( + packageLists: ListProperty + ): List { + val linksMap = + mapOf( + "android" to "https://developer.android.com/reference/kotlin/", + "google" to "https://developers.google.com/android/reference/", + "firebase" to "https://firebase.google.com/docs/reference/kotlin/", + "coroutines" to "https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/", + "kotlin" to "https://kotlinlang.org/api/latest/jvm/stdlib/" + ) + + return packageLists.get().map { + val externalLink = + linksMap[it.parentFile.nameWithoutExtension] + ?: throw RuntimeException("Unexpected package-list found: ${it.name}") + ExternalDocumentationLink(it, externalLink) } + } - private fun constructArguments(): JSONObject { - val jsonMap = mapOf( - "moduleName" to "", - "outputDir" to outputDirectory.get().path, - "globalLinks" to "", - "sourceSets" to listOf(mutableMapOf( - "sourceSetID" to mapOf( - "scopeId" to "androidx", - "sourceSetName" to "main" - ), - "sourceRoots" to sources.get().map { it.path }, - "classpath" to dependencies.get().map { it.path }, - "documentedVisibilities" to listOf("PUBLIC", "PROTECTED"), - "skipEmptyPackages" to "true", - "suppressedFiles" to suppressedFiles.get().map { it.path }, - "externalDocumentationLinks" to createExternalLinks(packageListFiles).map { mapOf( - "url" to it.externalLink, - "packageListUrl" to it.packageList.toURI() - ) } - )), - "offlineMode" to "true", - "noJdkLink" to "true" - ) - - return JSONObject(jsonMap) - } + private fun saveToJsonFile(jsonObject: JSONObject): File { + val outputFile = File.createTempFile("dackkaArgs", ".json") - private fun createExternalLinks(packageLists: ListProperty): List { - val linksMap = mapOf( - "android" to "https://developer.android.com/reference/kotlin/", - "google" to "https://developers.google.com/android/reference/", - "firebase" to "https://firebase.google.com/docs/reference/kotlin/", - "coroutines" to "https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/", - "kotlin" to "https://kotlinlang.org/api/latest/jvm/stdlib/" - ) - - return packageLists.get().map { - val externalLink = linksMap[it.parentFile.nameWithoutExtension] ?: throw RuntimeException("Unexpected package-list found: ${it.name}") - ExternalDocumentationLink(it, externalLink) - } - } + outputFile.deleteOnExit() + outputFile.writeText(jsonObject.toString(2)) - private fun saveToJsonFile(jsonObject: JSONObject): File { - val outputFile = File.createTempFile("dackkaArgs", ".json") - - outputFile.deleteOnExit() - outputFile.writeText(jsonObject.toString(2)) - - return outputFile - } + return outputFile + } - private fun launchDackka(clientName: Property, argsFile: File, workerExecutor: WorkerExecutor) { - val workQueue = workerExecutor.noIsolation() + private fun launchDackka( + clientName: Property, + argsFile: File, + workerExecutor: WorkerExecutor + ) { + val workQueue = workerExecutor.noIsolation() - workQueue.submit(DackkaWorkAction::class.java) { - args.set(listOf(argsFile.path, "-loggingLevel", "WARN")) - classpath.set(setOf(dackkaJarFile.get())) - projectName.set(clientName) - } + workQueue.submit(DackkaWorkAction::class.java) { + args.set(listOf(argsFile.path, "-loggingLevel", "WARN")) + classpath.set(setOf(dackkaJarFile.get())) + projectName.set(clientName) } + } } /** @@ -161,9 +181,9 @@ abstract class GenerateDocumentationTask @Inject constructor( * @property projectName name of the calling project, used for the devsite tenant (output directory) */ interface DackkaParams : WorkParameters { - val args: ListProperty - val classpath: SetProperty - val projectName: Property + val args: ListProperty + val classpath: SetProperty + val projectName: Property } /** @@ -171,16 +191,15 @@ interface DackkaParams : WorkParameters { * * Work actions are organized sections of work, offered by gradle. */ -abstract class DackkaWorkAction @Inject constructor( - private val execOperations: ExecOperations -) : WorkAction { - override fun execute() { - execOperations.javaexec { - mainClass.set("org.jetbrains.dokka.MainKt") - args = parameters.args.get() - classpath(parameters.classpath.get()) - - environment("DEVSITE_TENANT", "client/${parameters.projectName.get()}") - } +abstract class DackkaWorkAction @Inject constructor(private val execOperations: ExecOperations) : + WorkAction { + override fun execute() { + execOperations.javaexec { + mainClass.set("org.jetbrains.dokka.MainKt") + args = parameters.args.get() + classpath(parameters.classpath.get()) + + environment("DEVSITE_TENANT", "client/${parameters.projectName.get()}") } + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaPlugin.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaPlugin.kt index a7606f40a66..e0745ccfeb7 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaPlugin.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/DackkaPlugin.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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 import com.android.build.api.attributes.BuildTypeAttr @@ -14,345 +28,365 @@ import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.register /** -# Dackka Plugin - -The Dackka Plugin is a wrapper around internal tooling at Google called Dackka, which generates -documentation for [Firesite](https://firebase.google.com/docs/reference) - -## Dackka - -Dackka is an internal-purposed Dokka plugin. Google hosts its documentation on -an internal service called **Devsite**. Firebase hosts their documentation on a -variant of Devsite called **Firesite**. You can click [here](https://firebase.google.com/docs/reference) -to see how that looks. Essentially, it's just Google's way of decorating (and organizing) -documentation. - -Devsite expects its files to be in a very specific format. Previously, we would -use an internal Javadoc doclet called [Doclava](https://code.google.com/archive/p/doclava/) - which -allowed us to provide sensible defaults as to how the Javadoc should be -rendered. Then, we would do some further transformations to get the Javadoc -output in-line with what Devsite expects. This was a lengthy process, and came -with a lot of overhead. Furthermore, Doclava does not support kotlindoc and has -been unmaintained for many years. - -Dackka is an internal solution to that. Dackka provides a devsite plugin for -Dokka that will handle the job of doclava. Not only does this mean we can cut -out a huge portion of our transformation systems- but the overhead for maintaining -such systems is deferred away to the AndroidX team (the maintainers of Dackka). - -## Dackka Usage - -The Dackka we use is a fat jar pulled periodically from Dackka nightly builds, -and moved to our own maven repo bucket. Since it's recommended from the AndroidX -team to run Dackka on the command line, the fat jar allows us to ignore all the -miscenalionous dependencies of Dackka (in regards to Dokka especially). - -The general process of using Dackka is that you collect the dependencies and -source sets of the gradle project, create a -[Dokka appropriate JSON file](https://kotlin.github.io/dokka/1.7.10/user_guide/cli/usage/#example-using-json), -run the Dackka fat jar with the JSON file as an argument, and publish the -output folder. - -## Implementation - -Our implementation of Dackka falls into three separate files, and four separate -tasks. - -### [GenerateDocumentationTask] - -This task is the meat of our Dackka implementation. It's what actually handles -the running of Dackka itself. The task exposes a gradle extension called -[GenerateDocumentationTaskExtension] with various configuration points for -Dackka. This will likely be expanded upon in the future, as configurations are -needed. - -The job of this task is to **just** run Dackka. What happens after-the-fact does -not matter to this task. It will take the provided inputs, organize them into -the expected JSON file, and run Dackka with the JSON file as an argument. - -### [FiresiteTransformTask] - -Dackka was designed with Devsite in mind. The problem though, is that we use -Firesite. Firesite is very similar to Devsite, but there *are* minor differences. - -The job of this task is to transform the Dackka output from a Devsite purposed format, -to a Firesite purposed format. This includes removing unnecessary files, fixing -links, removing unnecessary headers, and so forth. - -There are open bugs for each transformation, as in an ideal world- they are instead -exposed as configurations from Dackka. Should these configurations be adopted by -Dackka, this task could become unnecessary itself- as we could just configure the task -during generation. - -### DackkaPlugin - -This plugin is the mind of our Dackka implementation. It manages registering, -and configuring all the tasks for Dackka (that is, the already established -tasks above). While we do not currently offer any configuration for the Dackka -plugin, this could change in the future as needed. Currently, the DackkaPlugin -provides sensible defaults to output directories, package lists, and so forth. - -The DackkaPlugin also provides four extra tasks: -[cleanDackkaDocumentation][registerCleanDackkaDocumentation], -[separateJavadocAndKotlinDoc][registerSeparateJavadocAndKotlinDoc] -[copyJavaDocToCommonDirectory][registerCopyJavaDocToCommonDirectoryTask] and -[copyKotlinDocToCommonDirectory][registerCopyKotlinDocToCommonDirectoryTask]. - -_cleanDackkaDocumentation_ is exactly what it sounds like, a task to clean up (delete) -the output of Dackka. This is useful when testing Dackka outputs itself- and -shouldn't be apart of the normal flow. The reasoning is that it would otherwise -invalidate the gradle cache. - -_separateJavadocAndKotlinDoc_ copies the Javadoc and Kotlindoc directories from Dackka into -their own subdirectories- for easy and consistent differentiation. - -_copyJavaDocToCommonDirectory_ copies the JavaDoc variant of the Dackka output for each sdk, -and pastes it in a common directory under the root project's build directory. This makes it easier -to zip the doc files for staging. - -_copyKotlinDocToCommonDirectory_ copies the KotlinDoc variant of the Dackka output for each sdk, -and pastes it in a common directory under the root project's build directory. This makes it easier -to zip the doc files for staging. - -Currently, the DackkaPlugin builds Java sources separate from Kotlin Sources. There is an open bug -for Dackka in which hidden parent classes and annotations do not hide themselves from children classes. -To work around this, we are currently generating stubs for Java sources via metalava, and feeding the stubs -to Dackka. This will be removed when the bug is fixed, per b/243954517 + * # Dackka Plugin + * + * The Dackka Plugin is a wrapper around internal tooling at Google called Dackka, which generates + * documentation for [Firesite](https://firebase.google.com/docs/reference) + * + * ## Dackka + * + * Dackka is an internal-purposed Dokka plugin. Google hosts its documentation on an internal + * service called **Devsite**. Firebase hosts their documentation on a variant of Devsite called + * **Firesite**. You can click [here](https://firebase.google.com/docs/reference) to see how that + * looks. Essentially, it's just Google's way of decorating (and organizing) documentation. + * + * Devsite expects its files to be in a very specific format. Previously, we would use an internal + * Javadoc doclet called [Doclava](https://code.google.com/archive/p/doclava/) + * - which allowed us to provide sensible defaults as to how the Javadoc should be rendered. Then, + * we would do some further transformations to get the Javadoc output in-line with what Devsite + * expects. This was a lengthy process, and came with a lot of overhead. Furthermore, Doclava does + * not support kotlindoc and has been unmaintained for many years. + * + * Dackka is an internal solution to that. Dackka provides a devsite plugin for Dokka that will + * handle the job of doclava. Not only does this mean we can cut out a huge portion of our + * transformation systems- but the overhead for maintaining such systems is deferred away to the + * AndroidX team (the maintainers of Dackka). + * + * ## Dackka Usage + * + * The Dackka we use is a fat jar pulled periodically from Dackka nightly builds, and moved to our + * own maven repo bucket. Since it's recommended from the AndroidX team to run Dackka on the command + * line, the fat jar allows us to ignore all the miscenalionous dependencies of Dackka (in regards + * to Dokka especially). + * + * The general process of using Dackka is that you collect the dependencies and source sets of the + * gradle project, create a + * [Dokka appropriate JSON file](https://kotlin.github.io/dokka/1.7.10/user_guide/cli/usage/#example-using-json) + * , run the Dackka fat jar with the JSON file as an argument, and publish the output folder. + * + * ## Implementation + * + * Our implementation of Dackka falls into three separate files, and four separate tasks. + * + * ### [GenerateDocumentationTask] + * + * This task is the meat of our Dackka implementation. It's what actually handles the running of + * Dackka itself. The task exposes a gradle extension called [GenerateDocumentationTaskExtension] + * with various configuration points for Dackka. This will likely be expanded upon in the future, as + * configurations are needed. + * + * The job of this task is to **just** run Dackka. What happens after-the-fact does not matter to + * this task. It will take the provided inputs, organize them into the expected JSON file, and run + * Dackka with the JSON file as an argument. + * + * ### [FiresiteTransformTask] + * + * Dackka was designed with Devsite in mind. The problem though, is that we use Firesite. Firesite + * is very similar to Devsite, but there *are* minor differences. + * + * The job of this task is to transform the Dackka output from a Devsite purposed format, to a + * Firesite purposed format. This includes removing unnecessary files, fixing links, removing + * unnecessary headers, and so forth. + * + * There are open bugs for each transformation, as in an ideal world- they are instead exposed as + * configurations from Dackka. Should these configurations be adopted by Dackka, this task could + * become unnecessary itself- as we could just configure the task during generation. + * + * ### DackkaPlugin + * + * This plugin is the mind of our Dackka implementation. It manages registering, and configuring all + * the tasks for Dackka (that is, the already established tasks above). While we do not currently + * offer any configuration for the Dackka plugin, this could change in the future as needed. + * Currently, the DackkaPlugin provides sensible defaults to output directories, package lists, and + * so forth. + * + * The DackkaPlugin also provides four extra tasks: [cleanDackkaDocumentation] + * [registerCleanDackkaDocumentation], [separateJavadocAndKotlinDoc] + * [registerSeparateJavadocAndKotlinDoc] [copyJavaDocToCommonDirectory] + * [registerCopyJavaDocToCommonDirectoryTask] and [copyKotlinDocToCommonDirectory] + * [registerCopyKotlinDocToCommonDirectoryTask]. + * + * _cleanDackkaDocumentation_ is exactly what it sounds like, a task to clean up (delete) the output + * of Dackka. This is useful when testing Dackka outputs itself- and shouldn't be apart of the + * normal flow. The reasoning is that it would otherwise invalidate the gradle cache. + * + * _separateJavadocAndKotlinDoc_ copies the Javadoc and Kotlindoc directories from Dackka into their + * own subdirectories- for easy and consistent differentiation. + * + * _copyJavaDocToCommonDirectory_ copies the JavaDoc variant of the Dackka output for each sdk, and + * pastes it in a common directory under the root project's build directory. This makes it easier to + * zip the doc files for staging. + * + * _copyKotlinDocToCommonDirectory_ copies the KotlinDoc variant of the Dackka output for each sdk, + * and pastes it in a common directory under the root project's build directory. This makes it + * easier to zip the doc files for staging. + * + * Currently, the DackkaPlugin builds Java sources separate from Kotlin Sources. There is an open + * bug for Dackka in which hidden parent classes and annotations do not hide themselves from + * children classes. To work around this, we are currently generating stubs for Java sources via + * metalava, and feeding the stubs to Dackka. This will be removed when the bug is fixed, per + * b/243954517 */ abstract class DackkaPlugin : Plugin { - override fun apply(project: Project) { - prepareJavadocConfiguration(project) - registerCleanDackkaDocumentation(project) - project.afterEvaluate { - if (shouldWePublish(project)) { - val dackkaOutputDirectory = project.provider { fileFromBuildDir("dackkaRawOutput") } - val separatedFilesDirectory = project.provider { fileFromBuildDir("dackkaSeparatedFiles") } - val transformedDackkaFilesDirectory = project.provider { fileFromBuildDir("dackkaTransformedFiles") } - - val generateDocumentation = registerGenerateDackkaDocumentationTask(project, dackkaOutputDirectory) - val separateJavadocAndKotlinDoc = registerSeparateJavadocAndKotlinDoc(project, dackkaOutputDirectory, separatedFilesDirectory) - val firesiteTransform = registerFiresiteTransformTask(project, separatedFilesDirectory, transformedDackkaFilesDirectory) - val copyJavaDocToCommonDirectory = registerCopyJavaDocToCommonDirectoryTask(project, transformedDackkaFilesDirectory) - val copyKotlinDocToCommonDirectory = registerCopyKotlinDocToCommonDirectoryTask(project, transformedDackkaFilesDirectory) - - project.tasks.register("kotlindoc") { - group = "documentation" - dependsOn( - generateDocumentation, - separateJavadocAndKotlinDoc, - firesiteTransform, - copyJavaDocToCommonDirectory, - copyKotlinDocToCommonDirectory - ) - } - } else { - project.tasks.register("kotlindoc") { - group = "documentation" - } - } + override fun apply(project: Project) { + prepareJavadocConfiguration(project) + registerCleanDackkaDocumentation(project) + project.afterEvaluate { + if (shouldWePublish(project)) { + val dackkaOutputDirectory = project.provider { fileFromBuildDir("dackkaRawOutput") } + val separatedFilesDirectory = project.provider { fileFromBuildDir("dackkaSeparatedFiles") } + val transformedDackkaFilesDirectory = + project.provider { fileFromBuildDir("dackkaTransformedFiles") } + + val generateDocumentation = + registerGenerateDackkaDocumentationTask(project, dackkaOutputDirectory) + val separateJavadocAndKotlinDoc = + registerSeparateJavadocAndKotlinDoc( + project, + dackkaOutputDirectory, + separatedFilesDirectory + ) + val firesiteTransform = + registerFiresiteTransformTask( + project, + separatedFilesDirectory, + transformedDackkaFilesDirectory + ) + val copyJavaDocToCommonDirectory = + registerCopyJavaDocToCommonDirectoryTask(project, transformedDackkaFilesDirectory) + val copyKotlinDocToCommonDirectory = + registerCopyKotlinDocToCommonDirectoryTask(project, transformedDackkaFilesDirectory) + + project.tasks.register("kotlindoc") { + group = "documentation" + dependsOn( + generateDocumentation, + separateJavadocAndKotlinDoc, + firesiteTransform, + copyJavaDocToCommonDirectory, + copyKotlinDocToCommonDirectory + ) } + } else { + project.tasks.register("kotlindoc") { group = "documentation" } + } } - - fun Project.firebaseConfigValue(getter: FirebaseLibraryExtension.() -> T): T = - project.extensions.getByType().getter() - - private fun shouldWePublish(project: Project) = - project.firebaseConfigValue { publishJavadoc } - - private fun prepareJavadocConfiguration(project: Project) { - val javadocConfig = project.javadocConfig - javadocConfig.dependencies += project.dependencies.create("com.google.code.findbugs:jsr305:3.0.2") - javadocConfig.dependencies += project.dependencies.create("com.google.errorprone:error_prone_annotations:2.15.0") - javadocConfig.attributes.attribute( - BuildTypeAttr.ATTRIBUTE, - project.objects.named(BuildTypeAttr::class.java, "release") - ) - } - - // TODO(b/243324828): Refactor when fixed, so we no longer need stubs - private fun registerGenerateDackkaDocumentationTask( - project: Project, - targetDirectory: Provider - ): Provider { - val docStubs = project.tasks.register("docStubsForDackkaInput") - val docsTask = project.tasks.register("generateDackkaDocumentation") - with(project.extensions.getByType()) { - libraryVariants.all { - if (name == "release") { - val isKotlin = project.plugins.hasPlugin("kotlin-android") - - val classpath = compileConfiguration.getJars() + project.javadocConfig.getJars() + project.files(bootClasspath) - - val sourceDirectories = sourceSets.flatMap { - it.javaDirectories.map { it.absoluteFile } - } - - docStubs.configure { - classPath = classpath - sources.set(project.provider { sourceDirectories }) - } - - docsTask.configure { - if (!isKotlin) dependsOn(docStubs) - - val packageLists = fetchPackageLists(project) - - val excludedFiles = projectSpecificSuppressedFiles(project) - val fixedSourceDirectories = if (!isKotlin) listOf(project.docStubs) else sourceDirectories - - sources.set(fixedSourceDirectories + projectSpecificSources(project)) - suppressedFiles.set(excludedFiles) - packageListFiles.set(packageLists) - - dependencies.set(classpath) - outputDirectory.set(targetDirectory) - - applyCommonConfigurations() - } - } - } + } + + fun Project.firebaseConfigValue(getter: FirebaseLibraryExtension.() -> T): T = + project.extensions.getByType().getter() + + private fun shouldWePublish(project: Project) = project.firebaseConfigValue { publishJavadoc } + + private fun prepareJavadocConfiguration(project: Project) { + val javadocConfig = project.javadocConfig + javadocConfig.dependencies += + project.dependencies.create("com.google.code.findbugs:jsr305:3.0.2") + javadocConfig.dependencies += + project.dependencies.create("com.google.errorprone:error_prone_annotations:2.15.0") + javadocConfig.attributes.attribute( + BuildTypeAttr.ATTRIBUTE, + project.objects.named(BuildTypeAttr::class.java, "release") + ) + } + + // TODO(b/243324828): Refactor when fixed, so we no longer need stubs + private fun registerGenerateDackkaDocumentationTask( + project: Project, + targetDirectory: Provider + ): Provider { + val docStubs = project.tasks.register("docStubsForDackkaInput") + val docsTask = project.tasks.register("generateDackkaDocumentation") + with(project.extensions.getByType()) { + libraryVariants.all { + if (name == "release") { + val isKotlin = project.plugins.hasPlugin("kotlin-android") + + val classpath = + compileConfiguration.getJars() + + project.javadocConfig.getJars() + + project.files(bootClasspath) + + val sourceDirectories = sourceSets.flatMap { it.javaDirectories.map { it.absoluteFile } } + + docStubs.configure { + classPath = classpath + sources.set(project.provider { sourceDirectories }) + } + + docsTask.configure { + if (!isKotlin) dependsOn(docStubs) + + val packageLists = fetchPackageLists(project) + + val excludedFiles = projectSpecificSuppressedFiles(project) + val fixedSourceDirectories = + if (!isKotlin) listOf(project.docStubs) else sourceDirectories + + sources.set(fixedSourceDirectories + projectSpecificSources(project)) + suppressedFiles.set(excludedFiles) + packageListFiles.set(packageLists) + + dependencies.set(classpath) + outputDirectory.set(targetDirectory) + + applyCommonConfigurations() + } } - return docsTask + } + } + return docsTask + } + + private fun fetchPackageLists(project: Project) = + project.rootProject + .fileTree("kotlindoc/package-lists") + .matching { include("**/package-list") } + .toList() + + // TODO(b/243534168): Remove when fixed + private fun projectSpecificSources(project: Project) = + when (project.name) { + "firebase-common" -> { + project.project(":firebase-firestore").files("src/main/java/com/google/firebase").toList() + } + else -> emptyList() } - private fun fetchPackageLists(project: Project) = - project.rootProject.fileTree("kotlindoc/package-lists").matching { - include("**/package-list") - }.toList() - - // TODO(b/243534168): Remove when fixed - private fun projectSpecificSources(project: Project) = - when (project.name) { - "firebase-common" -> { - project.project(":firebase-firestore").files("src/main/java/com/google/firebase").toList() - } - else -> emptyList() - } - - // TODO(b/243534168): Remove when fixed - private fun projectSpecificSuppressedFiles(project: Project): List = - when (project.name) { - "firebase-common" -> { - project.project(":firebase-firestore").files("src/main/java/com/google/firebase/firestore").toList() - } - "firebase-firestore" -> { - project.files("${project.docStubs}/com/google/firebase/Timestamp.java").toList() - } - else -> emptyList() - } - - private fun GenerateDocumentationTask.applyCommonConfigurations() { - dependsOnAndMustRunAfter("createFullJarRelease") - - val dackkaFile = project.provider { project.dackkaConfig.singleFile } - - dackkaJarFile.set(dackkaFile) - clientName.set(project.firebaseConfigValue { artifactId }) + // TODO(b/243534168): Remove when fixed + private fun projectSpecificSuppressedFiles(project: Project): List = + when (project.name) { + "firebase-common" -> { + project + .project(":firebase-firestore") + .files("src/main/java/com/google/firebase/firestore") + .toList() + } + "firebase-firestore" -> { + project.files("${project.docStubs}/com/google/firebase/Timestamp.java").toList() + } + else -> emptyList() } - // TODO(b/248302613): Remove when dackka exposes configuration for this - private fun registerSeparateJavadocAndKotlinDoc( - project: Project, - dackkaOutputDirectory: Provider, - outputDirectory: Provider - ): TaskProvider { - val outputJavadocFolder = project.childFile(outputDirectory, "android") - val outputKotlindocFolder = project.childFile(outputDirectory, "kotlin") + private fun GenerateDocumentationTask.applyCommonConfigurations() { + dependsOnAndMustRunAfter("createFullJarRelease") - val separateJavadoc = project.tasks.register("separateJavadoc") { - dependsOn("generateDackkaDocumentation") + val dackkaFile = project.provider { project.dackkaConfig.singleFile } - val javadocClientFolder = project.childFile(dackkaOutputDirectory, "reference/client") - val javadocComFolder = project.childFile(dackkaOutputDirectory, "reference/com") + dackkaJarFile.set(dackkaFile) + clientName.set(project.firebaseConfigValue { artifactId }) + } - fromDirectory(javadocClientFolder) - fromDirectory(javadocComFolder) + // TODO(b/248302613): Remove when dackka exposes configuration for this + private fun registerSeparateJavadocAndKotlinDoc( + project: Project, + dackkaOutputDirectory: Provider, + outputDirectory: Provider + ): TaskProvider { + val outputJavadocFolder = project.childFile(outputDirectory, "android") + val outputKotlindocFolder = project.childFile(outputDirectory, "kotlin") - into(outputJavadocFolder) - } + val separateJavadoc = + project.tasks.register("separateJavadoc") { + dependsOn("generateDackkaDocumentation") - val separateKotlindoc = project.tasks.register("separateKotlindoc") { - dependsOn("generateDackkaDocumentation") + val javadocClientFolder = project.childFile(dackkaOutputDirectory, "reference/client") + val javadocComFolder = project.childFile(dackkaOutputDirectory, "reference/com") - val kotlindocFolder = project.childFile(dackkaOutputDirectory, "reference/kotlin") + fromDirectory(javadocClientFolder) + fromDirectory(javadocComFolder) - from(kotlindocFolder) + into(outputJavadocFolder) + } - into(outputKotlindocFolder) - } + val separateKotlindoc = + project.tasks.register("separateKotlindoc") { + dependsOn("generateDackkaDocumentation") - return project.tasks.register("separateJavadocAndKotlinDoc") { - dependsOn(separateJavadoc, separateKotlindoc) - } - } + val kotlindocFolder = project.childFile(dackkaOutputDirectory, "reference/kotlin") - private fun registerFiresiteTransformTask( - project: Project, - separatedFilesDirectory: Provider, - targetDirectory: Provider - ): TaskProvider { - val transformJavadoc = project.tasks.register("firesiteTransformJavadoc") { - dependsOnAndMustRunAfter("separateJavadoc") - - referenceHeadTagsPath.set("docs/reference/android") - dackkaFiles.set(project.childFile(separatedFilesDirectory, "android")) - outputDirectory.set(project.childFile(targetDirectory, "android")) - } + from(kotlindocFolder) - val transformKotlindoc = project.tasks.register("firesiteTransformKotlindoc") { - dependsOnAndMustRunAfter("separateKotlindoc") + into(outputKotlindocFolder) + } - referenceHeadTagsPath.set("docs/reference/kotlin") - dackkaFiles.set(project.childFile(separatedFilesDirectory, "kotlin")) - outputDirectory.set(project.childFile(targetDirectory, "kotlin")) - } - - return project.tasks.register("firesiteTransform") { - dependsOn(transformJavadoc, transformKotlindoc) - } + return project.tasks.register("separateJavadocAndKotlinDoc") { + dependsOn(separateJavadoc, separateKotlindoc) + } + } + + private fun registerFiresiteTransformTask( + project: Project, + separatedFilesDirectory: Provider, + targetDirectory: Provider + ): TaskProvider { + val transformJavadoc = + project.tasks.register("firesiteTransformJavadoc") { + dependsOnAndMustRunAfter("separateJavadoc") + + removeGoogleGroupId.set(true) + referencePath.set("/docs/reference/android") + referenceHeadTagsPath.set("docs/reference/android") + dackkaFiles.set(project.childFile(separatedFilesDirectory, "android")) + outputDirectory.set(project.childFile(targetDirectory, "android")) + } + + val transformKotlindoc = + project.tasks.register("firesiteTransformKotlindoc") { + dependsOnAndMustRunAfter("separateKotlindoc") + + referenceHeadTagsPath.set("docs/reference/kotlin") + referencePath.set("/docs/reference/kotlin") + dackkaFiles.set(project.childFile(separatedFilesDirectory, "kotlin")) + outputDirectory.set(project.childFile(targetDirectory, "kotlin")) + } + + return project.tasks.register("firesiteTransform") { + dependsOn(transformJavadoc, transformKotlindoc) } + } - // TODO(b/246593212): Migrate doc files to single directory - private fun registerCopyJavaDocToCommonDirectoryTask(project: Project, outputDirectory: Provider) = - project.tasks.register("copyJavaDocToCommonDirectory") { - /** - * This is not currently cache compliant. The need for this property is - * temporary while we test it alongside the current javaDoc task. Since it's such a - * temporary behavior, losing cache compliance is fine for now. - */ - if (project.rootProject.findProperty("dackkaJavadoc") == "true") { - mustRunAfter("firesiteTransform") + // TODO(b/246593212): Migrate doc files to single directory + private fun registerCopyJavaDocToCommonDirectoryTask( + project: Project, + outputDirectory: Provider + ) = + project.tasks.register("copyJavaDocToCommonDirectory") { + mustRunAfter("firesiteTransform") - val outputFolder = project.rootProject.fileFromBuildDir("firebase-kotlindoc/android") - val javaFolder = project.childFile(outputDirectory, "android") + val outputFolder = project.rootProject.fileFromBuildDir("firebase-kotlindoc") + val javaFolder = project.childFile(outputDirectory, "android") - fromDirectory(javaFolder) + fromDirectory(javaFolder) - into(outputFolder) - } - } + into(outputFolder) + } - // TODO(b/246593212): Migrate doc files to single directory - private fun registerCopyKotlinDocToCommonDirectoryTask(project: Project, outputDirectory: Provider) = - project.tasks.register("copyKotlinDocToCommonDirectory") { - mustRunAfter("firesiteTransform") + // TODO(b/246593212): Migrate doc files to single directory + private fun registerCopyKotlinDocToCommonDirectoryTask( + project: Project, + outputDirectory: Provider + ) = + project.tasks.register("copyKotlinDocToCommonDirectory") { + mustRunAfter("firesiteTransform") - val outputFolder = project.rootProject.fileFromBuildDir("firebase-kotlindoc") - val kotlinFolder = project.childFile(outputDirectory, "kotlin") + val outputFolder = project.rootProject.fileFromBuildDir("firebase-kotlindoc") + val kotlinFolder = project.childFile(outputDirectory, "kotlin") - fromDirectory(kotlinFolder) + fromDirectory(kotlinFolder) - into(outputFolder) - } + into(outputFolder) + } - // Useful for local testing, but may not be desired for standard use (that's why it's not depended on) - private fun registerCleanDackkaDocumentation(project: Project) = - project.tasks.register("cleanDackkaDocumentation") { - group = "cleanup" + // Useful for local testing, but may not be desired for standard use (that's why it's not depended + // on) + private fun registerCleanDackkaDocumentation(project: Project) = + project.tasks.register("cleanDackkaDocumentation") { + group = "cleanup" - val outputDirs = listOf("dackkaRawOutput", "dackkaSeparatedFiles", "dackkaTransformedFiles") + val outputDirs = listOf("dackkaRawOutput", "dackkaSeparatedFiles", "dackkaTransformedFiles") - delete(outputDirs.map { project.fileFromBuildDir(it) }) - delete(project.rootProject.fileFromBuildDir("firebase-kotlindoc")) - } + delete(outputDirs.map { project.fileFromBuildDir(it) }) + delete(project.rootProject.fileFromBuildDir("firebase-kotlindoc")) + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.java b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.java index 9c507fa7536..2e2438a3fbc 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseJavaLibraryPlugin.java @@ -68,13 +68,6 @@ private static void setupStaticAnalysis(Project project, FirebaseLibraryExtensio .getConfigurations() .all( c -> { - if ("annotationProcessor".equals(c.getName())) { - for (String checkProject : library.staticAnalysis.errorproneCheckProjects) { - project - .getDependencies() - .add("annotationProcessor", project.project(checkProject)); - } - } if ("lintChecks".equals(c.getName())) { for (String checkProject : library.staticAnalysis.androidLintCheckProjects) { diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.java b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.java index f934dbcdde2..559c7e2017e 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryExtension.java @@ -91,7 +91,6 @@ public FirebaseLibraryExtension(Project project, LibraryType type) { private FirebaseStaticAnalysis initializeStaticAnalysis(Project project) { return new FirebaseStaticAnalysis( - projectsFromProperty(project, "firebase.checks.errorproneProjects"), projectsFromProperty(project, "firebase.checks.lintProjects")); } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java index 87374d14265..b4b9f89c77a 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.java @@ -213,13 +213,6 @@ private static void setupStaticAnalysis(Project project, FirebaseLibraryExtensio .getConfigurations() .all( c -> { - if ("annotationProcessor".equals(c.getName())) { - for (String checkProject : library.staticAnalysis.errorproneCheckProjects) { - project - .getDependencies() - .add("annotationProcessor", project.project(checkProject)); - } - } if ("lintChecks".equals(c.getName())) { for (String checkProject : library.staticAnalysis.androidLintCheckProjects) { diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseProprietaryLibraryPlugin.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseProprietaryLibraryPlugin.kt index 06d024ecaec..eb1d89f918d 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseProprietaryLibraryPlugin.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseProprietaryLibraryPlugin.kt @@ -19,18 +19,18 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.apply class FirebaseProprietaryLibraryPlugin : Plugin { - override fun apply(project: Project) { - project.apply(plugin = "firebase-library") + override fun apply(project: Project) { + project.apply(plugin = "firebase-library") - val library = project.extensions.getByType(FirebaseLibraryExtension::class.java) - library.publishSources = false - library.customizePom { - licenses { - license { - name.set("Android Software Development Kit License") - url.set("https://developer.android.com/studio/terms.html") - } - } + val library = project.extensions.getByType(FirebaseLibraryExtension::class.java) + library.publishSources = false + library.customizePom { + licenses { + license { + name.set("Android Software Development Kit License") + url.set("https://developer.android.com/studio/terms.html") } + } } + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseStaticAnalysis.java b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseStaticAnalysis.java index 1f8da8b03b9..374a66f2fb3 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseStaticAnalysis.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseStaticAnalysis.java @@ -17,12 +17,9 @@ import java.util.Set; public class FirebaseStaticAnalysis { - public Set errorproneCheckProjects; public Set androidLintCheckProjects; - public FirebaseStaticAnalysis( - Set errorproneCheckProjects, Set androidLintCheckProjects) { - this.errorproneCheckProjects = errorproneCheckProjects; + public FirebaseStaticAnalysis(Set androidLintCheckProjects) { this.androidLintCheckProjects = androidLintCheckProjects; } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt index c68279d5985..0a7641c7583 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FiresiteTransformTask.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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 import java.io.File @@ -6,6 +20,7 @@ import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity @@ -14,93 +29,148 @@ import org.gradle.api.tasks.TaskAction /** * Fixes minor inconsistencies between what dackka generates, and what firesite actually expects. * - * Should dackka ever expand to offer configurations for these procedures, this class can be replaced. + * Should dackka ever expand to offer configurations for these procedures, this class can be + * replaced. * * More specifically, it: - * - Deletes unnecessary files - * - Removes Class and Index headers from _toc.yaml files - * - Appends /docs/ to hyperlinks in html files - * - Removes the prefix path from book_path - * - Removes the firebase prefix from all links - * - Changes the path for _reference-head-tags at the top of html files + * - Deletes unnecessary files + * - Removes Class and Index headers from _toc.yaml files + * - Changes links to be appropriate for Firesite versus normal Devsite behavior + * - Fixes broken hyperlinks in `@see` blocks + * - Removes the prefix path from book_path + * - Removes the google groupId for Javadocs + * - Changes the path for _reference-head-tags at the top of html files * - * **Please note:** - * This task is idempotent- meaning it can safely be ran multiple times on the same set of files. + * **Please note:** This task is idempotent- meaning it can safely be ran multiple times on the same + * set of files. */ @CacheableTask abstract class FiresiteTransformTask : DefaultTask() { - @get:InputDirectory - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val dackkaFiles: Property - - @get:Input - abstract val referenceHeadTagsPath: Property - - @get:OutputDirectory - abstract val outputDirectory: Property - - @TaskAction - fun build() { - val namesOfFilesWeDoNotNeed = listOf( - "index.html", - "classes.html", - "packages.html", - "package-list" - ) - val rootDirectory = dackkaFiles.get() - val targetDirectory = outputDirectory.get() - targetDirectory.deleteRecursively() - - rootDirectory.walkTopDown().forEach { - if (it.name !in namesOfFilesWeDoNotNeed) { - val relativePath = it.toRelativeString(rootDirectory) - val newFile = it.copyTo(File("${targetDirectory.path}/$relativePath"), true) - - when (it.extension) { - "html" -> newFile.fixHTMLFile() - "yaml" -> newFile.fixYamlFile() - } - } + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val dackkaFiles: Property + + @get:Input abstract val referenceHeadTagsPath: Property + + @get:Input abstract val referencePath: Property + + @get:Input @get:Optional abstract val removeGoogleGroupId: Property + + @get:OutputDirectory abstract val outputDirectory: Property + + @TaskAction + fun build() { + val namesOfFilesWeDoNotNeed = + listOf("index.html", "classes.html", "packages.html", "package-list") + val rootDirectory = dackkaFiles.get() + val targetDirectory = outputDirectory.get() + targetDirectory.deleteRecursively() + + rootDirectory.walkTopDown().forEach { + if (it.name !in namesOfFilesWeDoNotNeed) { + val relativePath = it.toRelativeString(rootDirectory) + val newFile = it.copyTo(File("${targetDirectory.path}/$relativePath"), true) + + when (it.extension) { + "html" -> newFile.fixHTMLFile() + "yaml" -> newFile.fixYamlFile() } + } } + } - private fun File.fixHTMLFile() { - val fixedContent = readText().fixBookPath().fixHyperlinks().removeLeadingFirebaseDomainInLinks().fixReferenceHeadTagsPath() - writeText(fixedContent) - } + private fun File.fixHTMLFile() { + val fixedContent = + readText().fixBookPath().fixReferenceHeadTagsPath().fixLinks().fixHyperlinksInSeeBlocks() + writeText(fixedContent) + } + + private fun File.fixYamlFile() { + val fixedContent = + readText().removeClassHeader().removeIndexHeader().fixLinks().let { + if (removeGoogleGroupId.getOrElse(false)) it.removeGoogleGroupId() else it + } + writeText(fixedContent) + } + + /** + * Removes the leading `com.google` group id from strings in the file + * + * We have internal SDKs that generate their docs outside the scope of this plugin. The Javadoc + * variant of those SDks is typically generated with metalava- which does *not* provide the + * groupId. This makes the output look weird, as not all SDKs line up. So this method exists to + * correct Javadoc nav files, so that they align with internally generated docs. + * + * Example input: + * ``` + * "com.google.firebase.appcheck" + * ``` + * Example output: + * ``` + * "firebase.appcheck" + * ``` + */ + // TODO(b/257293594): Remove when dackka exposes configuration for this + private fun String.removeGoogleGroupId() = remove(Regex("(?<=\")com.google.(?=firebase.)")) + + /** + * Fixes broken hyperlinks in the rendered HTML + * + * Links in Dockka are currently broken in `@see` tags. This transform destructures those broken + * links and reconstructs them as they're expected to be. + * + * Example input: + * ``` + * // Generated from @see TimestampThe ref timestamp definition + * <a href="git.page.link/timestamp-proto">Timestamp</a>The ref timestamp definition + * + * ``` + * Example output: + * ``` + * Timestamp + * The ref timestamp definition + * ``` + */ + // TODO(go/dokka-upstream-bug/2665): Remove when Dockka fixes this issue + private fun String.fixHyperlinksInSeeBlocks() = + replace( + Regex( + "<a href="(?.*)">(?.*)</a>(?.*)\\s*" + ) + ) { + val (href, link, text) = it.destructured - private fun File.fixYamlFile() { - val fixedContent = readText().removeClassHeader().removeIndexHeader().removeLeadingFirebaseDomainInLinks() - writeText(fixedContent) + """ + $link + $text + """ + .trimIndent() } - // We utilize difference reference head tags between Kotlin and Java docs - // TODO(b/248316730): Remove when dackka exposes configuration for this - private fun String.fixReferenceHeadTagsPath() = - replace(Regex("(?<=include \").*(?=/_reference-head-tags.html\" %})"), referenceHeadTagsPath.get()) - - // We don't actually upload class or index files, - // so these headers will throw not found errors if not removed. - // TODO(b/243674302): Remove when dackka exposes configuration for this - private fun String.removeClassHeader() = - remove(Regex("- title: \"Class Index\"\n {2}path: \".+\"\n\n")) - private fun String.removeIndexHeader() = - remove(Regex("- title: \"Package Index\"\n {2}path: \".+\"\n\n")) - - // We use a common book for all sdks, wheres dackka expects each sdk to have its own book. - // TODO(b/243674303): Remove when dackka exposes configuration for this - private fun String.fixBookPath() = - remove(Regex("(?<=setvar book_path ?%})(.+)(?=/_book.yaml\\{% ?endsetvar)")) - - // Our documentation lives under /docs/reference/ versus the expected /reference/ - // TODO(b/243674305): Remove when dackka exposes configuration for this - private fun String.fixHyperlinks() = - replace(Regex("(?<=href=\")(/)(?=reference/.*\\.html)"), "/docs/") - - // The documentation will work fine without this. This is primarily to make sure that links - // resolve to their local counter part. Meaning when the docs are staged, they will resolve to - // staged docs instead of prod docs- and vise versa. - // TODO(b/243673063): Remove when dackka exposes configuration for this - private fun String.removeLeadingFirebaseDomainInLinks() = - remove(Regex("(?<=\")(https://firebase\\.google\\.com)(?=/docs/reference)")) + // Our documentation does not live under the standard path expected by Dackka, especially + // between Kotlin + Javadocs + // TODO(b/243674305): Remove when dackka exposes configuration for this + private fun String.fixLinks() = + replace(Regex("(?<=\")/reference[^\"]*?(?=/com/google/firebase)"), referencePath.get()) + + // We utilize difference reference head tags between Kotlin and Java docs + // TODO(b/248316730): Remove when dackka exposes configuration for this + private fun String.fixReferenceHeadTagsPath() = + replace( + Regex("(?<=include \").*(?=/_reference-head-tags.html\" %})"), + referenceHeadTagsPath.get() + ) + + // We don't actually upload class or index files, + // so these headers will throw not found errors if not removed. + // TODO(b/243674302): Remove when dackka exposes configuration for this + private fun String.removeClassHeader() = + remove(Regex("- title: \"Class Index\"\n {2}path: \".+\"\n\n")) + private fun String.removeIndexHeader() = + remove(Regex("- title: \"Package Index\"\n {2}path: \".+\"\n\n")) + + // We use a common book for all sdks, wheres dackka expects each sdk to have its own book. + // TODO(b/243674303): Remove when dackka exposes configuration for this + private fun String.fixBookPath() = + remove(Regex("(?<=setvar book_path ?%})(.+)(?=/_book.yaml\\{% ?endsetvar)")) } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/GradleUtils.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/GradleUtils.kt index 21abb2d98b4..eb0dad74404 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/GradleUtils.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/GradleUtils.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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 import java.io.File @@ -6,9 +20,7 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.Copy fun Copy.fromDirectory(directory: Provider) = - from(directory) { - into(directory.map { it.name }) - } + from(directory) { into(directory.map { it.name }) } /** * Creates a file at the buildDir for the given [Project]. @@ -30,6 +42,5 @@ fun Project.fileFromBuildDir(path: String) = file("$buildDir/$path") * fileProvider.map { project.file("${it.path}/$path") } * ``` */ -fun Project.childFile(provider: Provider, childPath: String) = provider.map { - file("${it.path}/$childPath") -} +fun Project.childFile(provider: Provider, childPath: String) = + provider.map { file("${it.path}/$childPath") } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/KotlinUtils.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/KotlinUtils.kt index b1bd5d88520..0045afeef87 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/KotlinUtils.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/KotlinUtils.kt @@ -1,11 +1,21 @@ +// Copyright 2022 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 -/** - * Replaces all matching substrings with an empty string (nothing) - */ +/** Replaces all matching substrings with an empty string (nothing) */ fun String.remove(regex: Regex) = replace(regex, "") -/** - * Replaces all matching substrings with an empty string (nothing) - */ +/** Replaces all matching substrings with an empty string (nothing) */ fun String.remove(str: String) = replace(str, "") diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt index 6ea25710bbd..aeb2335ecf3 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt @@ -32,190 +32,162 @@ import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction val Project.metalavaConfig: Configuration - get() = - configurations.findByName("metalavaArtifacts") - ?: configurations.create("metalavaArtifacts") { - this.dependencies.add(this@metalavaConfig.dependencies.create("com.android.tools.metalava:metalava:1.0.0-alpha06")) - } + get() = + configurations.findByName("metalavaArtifacts") + ?: configurations.create("metalavaArtifacts") { + this.dependencies.add( + this@metalavaConfig.dependencies.create( + "com.android.tools.metalava:metalava:1.0.0-alpha06" + ) + ) + } val Project.docStubs: File? - get() = - project.file("${buildDir.path}/doc-stubs") + get() = project.file("${buildDir.path}/doc-stubs") fun Project.runMetalavaWithArgs( - arguments: List, - ignoreFailure: Boolean = false, - stdOut: OutputStream? = null + arguments: List, + ignoreFailure: Boolean = false, + stdOut: OutputStream? = null ) { - val allArgs = listOf( - "--no-banner", - "--hide", - "HiddenSuperclass", // We allow having a hidden parent class - "--hide", - "HiddenAbstractMethod" + val allArgs = + listOf( + "--no-banner", + "--hide", + "HiddenSuperclass", // We allow having a hidden parent class + "--hide", + "HiddenAbstractMethod" ) + arguments - project.javaexec { - main = "com.android.tools.metalava.Driver" - classpath = project.metalavaConfig - args = allArgs - isIgnoreExitValue = ignoreFailure - if (stdOut != null) errorOutput = stdOut - } + project.javaexec { + main = "com.android.tools.metalava.Driver" + classpath = project.metalavaConfig + args = allArgs + isIgnoreExitValue = ignoreFailure + if (stdOut != null) errorOutput = stdOut + } } abstract class GenerateStubsTask : DefaultTask() { - /** Source files against which API signatures will be validated. */ - @get:InputFiles - abstract val sources: SetProperty - - @get:[InputFiles Classpath] - lateinit var classPath: FileCollection - - @get:OutputDirectory - val outputDir: File = File(project.buildDir, "doc-stubs") - - @TaskAction - fun run() { - val sourcePath = sources.get().asSequence() - .filter { it.exists() } - .map { it.absolutePath } - .joinToString(":") - - val classPath = classPath.files.asSequence() - .map { it.absolutePath }.toMutableList() - project.androidJar?.let { - classPath += listOf(it.absolutePath) - } - - project.runMetalavaWithArgs( - listOf( - "--source-path", - sourcePath, - "--classpath", - classPath.joinToString(":"), - "--include-annotations", - "--doc-stubs", - outputDir.absolutePath - ) - ) - } + /** Source files against which API signatures will be validated. */ + @get:InputFiles abstract val sources: SetProperty + + @get:[InputFiles Classpath] + lateinit var classPath: FileCollection + + @get:OutputDirectory val outputDir: File = File(project.buildDir, "doc-stubs") + + @TaskAction + fun run() { + val sourcePath = + sources.get().asSequence().filter { it.exists() }.map { it.absolutePath }.joinToString(":") + + val classPath = classPath.files.asSequence().map { it.absolutePath }.toMutableList() + project.androidJar?.let { classPath += listOf(it.absolutePath) } + + project.runMetalavaWithArgs( + listOf( + "--source-path", + sourcePath, + "--classpath", + classPath.joinToString(":"), + "--include-annotations", + "--doc-stubs", + outputDir.absolutePath + ) + ) + } } abstract class GenerateApiTxtTask : DefaultTask() { - /** Source files against which API signatures will be validated. */ - @get:InputFiles - abstract val sources: SetProperty - - @get:InputFiles - lateinit var classPath: FileCollection - - @get:OutputFile - abstract val apiTxtFile: Property - - @get:OutputFile - abstract val baselineFile: Property - - @get:Input - abstract val updateBaseline: Property - - @TaskAction - fun run() { - val sourcePath = sources.get().asSequence() - .filter { it.exists() } - .map { it.absolutePath } - .joinToString(":") - - val classPath = classPath.files.asSequence() - .map { it.absolutePath }.toMutableList() - project.androidJar?.let { - classPath += listOf(it.absolutePath) - } - - project.runMetalavaWithArgs( - listOf( - "--source-path", - sourcePath, - "--classpath", - classPath.joinToString(":"), - "--api", - apiTxtFile.get().absolutePath, - "--format=v2" - ) + if (updateBaseline.get()) listOf("--update-baseline") else if (baselineFile.get() - .exists() - ) listOf( - "--baseline", - baselineFile.get().absolutePath - ) else listOf(), - ignoreFailure = true - ) - } + /** Source files against which API signatures will be validated. */ + @get:InputFiles abstract val sources: SetProperty + + @get:InputFiles lateinit var classPath: FileCollection + + @get:OutputFile abstract val apiTxtFile: Property + + @get:OutputFile abstract val baselineFile: Property + + @get:Input abstract val updateBaseline: Property + + @TaskAction + fun run() { + val sourcePath = + sources.get().asSequence().filter { it.exists() }.map { it.absolutePath }.joinToString(":") + + val classPath = classPath.files.asSequence().map { it.absolutePath }.toMutableList() + project.androidJar?.let { classPath += listOf(it.absolutePath) } + + project.runMetalavaWithArgs( + listOf( + "--source-path", + sourcePath, + "--classpath", + classPath.joinToString(":"), + "--api", + apiTxtFile.get().absolutePath, + "--format=v2" + ) + + if (updateBaseline.get()) listOf("--update-baseline") + else if (baselineFile.get().exists()) listOf("--baseline", baselineFile.get().absolutePath) + else listOf(), + ignoreFailure = true + ) + } } abstract class ApiInformationTask : DefaultTask() { - /** Source files against which API signatures will be validated. */ - @get:InputFiles - abstract val sources: SetProperty - - @get:InputFiles - lateinit var classPath: FileCollection - - @get:InputFile - abstract val apiTxtFile: Property - - @get:OutputFile - abstract val outputApiFile: Property - - @get:OutputFile - abstract val baselineFile: Property - - @get:OutputFile - abstract val outputFile: Property - - @get:Input - abstract val updateBaseline: Property - - @TaskAction - fun run() { - val sourcePath = sources.get().asSequence() - .filter { it.exists() } - .map { it.absolutePath } - .joinToString(":") - - val classPath = classPath.files.asSequence() - .map { it.absolutePath }.toMutableList() - project.androidJar?.let { - classPath += listOf(it.absolutePath) - } - - project.runMetalavaWithArgs( - listOf( - "--source-path", - sourcePath, - "--classpath", - classPath.joinToString(":"), - "--api", - outputApiFile.get().absolutePath, - "--format=v2" - ), - ignoreFailure = true - ) - - project.runMetalavaWithArgs( - listOf( - "--source-files", - outputApiFile.get().absolutePath, - "--check-compatibility:api:released", - apiTxtFile.get().absolutePath, - "--format=v2", - "--no-color" - ) + if (updateBaseline.get()) listOf("--update-baseline") else if (baselineFile.get() - .exists() - ) listOf( - "--baseline", - baselineFile.get().absolutePath - ) else listOf(), - ignoreFailure = true, stdOut = FileOutputStream(outputFile.get()) - ) - } + /** Source files against which API signatures will be validated. */ + @get:InputFiles abstract val sources: SetProperty + + @get:InputFiles lateinit var classPath: FileCollection + + @get:InputFile abstract val apiTxtFile: Property + + @get:OutputFile abstract val outputApiFile: Property + + @get:OutputFile abstract val baselineFile: Property + + @get:OutputFile abstract val outputFile: Property + + @get:Input abstract val updateBaseline: Property + + @TaskAction + fun run() { + val sourcePath = + sources.get().asSequence().filter { it.exists() }.map { it.absolutePath }.joinToString(":") + + val classPath = classPath.files.asSequence().map { it.absolutePath }.toMutableList() + project.androidJar?.let { classPath += listOf(it.absolutePath) } + + project.runMetalavaWithArgs( + listOf( + "--source-path", + sourcePath, + "--classpath", + classPath.joinToString(":"), + "--api", + outputApiFile.get().absolutePath, + "--format=v2" + ), + ignoreFailure = true + ) + + project.runMetalavaWithArgs( + listOf( + "--source-files", + outputApiFile.get().absolutePath, + "--check-compatibility:api:released", + apiTxtFile.get().absolutePath, + "--format=v2", + "--no-color" + ) + + if (updateBaseline.get()) listOf("--update-baseline") + else if (baselineFile.get().exists()) listOf("--baseline", baselineFile.get().absolutePath) + else listOf(), + ignoreFailure = true, + stdOut = FileOutputStream(outputFile.get()) + ) + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ProjectUtils.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ProjectUtils.kt index d79299fff1a..1e62eb105e5 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ProjectUtils.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ProjectUtils.kt @@ -20,19 +20,27 @@ import org.gradle.api.attributes.Attribute import org.gradle.api.provider.Provider fun Project.isAndroid(): Boolean = - listOf("com.android.application", "com.android.library", "com.android.test") - .any { plugin: String -> project.plugins.hasPlugin(plugin) } + listOf("com.android.application", "com.android.library", "com.android.test").any { plugin: String + -> + project.plugins.hasPlugin(plugin) + } fun toBoolean(value: Any?): Boolean { - val trimmed = value?.toString()?.trim()?.toLowerCase() - return "true" == trimmed || "y" == trimmed || "1" == trimmed + val trimmed = value?.toString()?.trim()?.toLowerCase() + return "true" == trimmed || "y" == trimmed || "1" == trimmed } -/** - * Finds or creates the javadocClasspath [Configuration]. - */ +/** Finds or creates the javadocClasspath [Configuration]. */ val Project.javadocConfig: Configuration - get() = configurations.findByName("javadocClasspath") ?: configurations.create("javadocClasspath") + get() = + configurations.findByName("javadocClasspath") + ?: configurations.create("javadocClasspath").also { javadocClasspath -> + configurations.all { + if (name == "compileOnly") { + javadocClasspath.extendsFrom(this) + } + } + } /** * Finds or creates the dackkaArtifacts [Configuration]. @@ -41,40 +49,37 @@ val Project.javadocConfig: Configuration * dependencies. */ val Project.dackkaConfig: Configuration - get() = - configurations.findByName("dackkaArtifacts") ?: configurations.create("dackkaArtifacts") { - dependencies.add(this@dackkaConfig.dependencies.create("com.google.devsite:dackka-fat:1.0.3")) - } + get() = + configurations.findByName("dackkaArtifacts") + ?: configurations.create("dackkaArtifacts") { + dependencies.add( + this@dackkaConfig.dependencies.create("com.google.devsite:dackka-fat:1.0.3") + ) + } -/** - * Fetches the jars of dependencies associated with this configuration through an artifact view. - */ -fun Configuration.getJars() = incoming.artifactView { - attributes { - attribute(Attribute.of("artifactType", String::class.java), "android-classes") +/** Fetches the jars of dependencies associated with this configuration through an artifact view. */ +fun Configuration.getJars() = + incoming + .artifactView { + attributes { attribute(Attribute.of("artifactType", String::class.java), "android-classes") } } -}.artifacts.artifactFiles + .artifacts + .artifactFiles -/** - * Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task - */ +/** Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task */ fun T.dependsOnAndMustRunAfter(otherTask: R) { - mustRunAfter(otherTask) - dependsOn(otherTask) + mustRunAfter(otherTask) + dependsOn(otherTask) } -/** - * Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task - */ +/** Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task */ fun T.dependsOnAndMustRunAfter(otherTask: Provider) { - mustRunAfter(otherTask) - dependsOn(otherTask) + mustRunAfter(otherTask) + dependsOn(otherTask) } -/** - * Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task name - */ +/** Utility method to call [Task.mustRunAfter] and [Task.dependsOn] on the specified task name */ fun T.dependsOnAndMustRunAfter(otherTask: String) { - mustRunAfter(otherTask) - dependsOn(otherTask) + mustRunAfter(otherTask) + dependsOn(otherTask) } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt index b3fdfccc060..ebfc4f7d67f 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/SdkUtil.kt @@ -22,29 +22,27 @@ import org.gradle.api.GradleException import org.gradle.api.Project val Project.sdkDir: File - get() { - val properties = Properties() - val localProperties = rootProject.file("local.properties") - if (localProperties.exists()) { - try { - FileInputStream(localProperties).use { fis -> properties.load(fis) } - } catch (ex: IOException) { - throw GradleException("Could not load local.properties", ex) - } - } - val sdkDir = properties.getProperty("sdk.dir") - if (sdkDir != null) { - return file(sdkDir) - } - val androidHome = System.getenv("ANDROID_HOME") - ?: throw GradleException("No sdk.dir or ANDROID_HOME set.") - return file(androidHome) + get() { + val properties = Properties() + val localProperties = rootProject.file("local.properties") + if (localProperties.exists()) { + try { + FileInputStream(localProperties).use { fis -> properties.load(fis) } + } catch (ex: IOException) { + throw GradleException("Could not load local.properties", ex) + } } + val sdkDir = properties.getProperty("sdk.dir") + if (sdkDir != null) { + return file(sdkDir) + } + val androidHome = + System.getenv("ANDROID_HOME") ?: throw GradleException("No sdk.dir or ANDROID_HOME set.") + return file(androidHome) + } val Project.androidJar: File? - get() { - val android = project.extensions.findByType(LibraryExtension::class.java) - ?: return null - return File( - sdkDir, String.format("/platforms/%s/android.jar", android.compileSdkVersion)) - } + get() { + val android = project.extensions.findByType(LibraryExtension::class.java) ?: return null + return File(sdkDir, String.format("/platforms/%s/android.jar", android.compileSdkVersion)) + } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/VendorPlugin.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/VendorPlugin.kt index 60c4e067392..da1a0659fbd 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/VendorPlugin.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/VendorPlugin.kt @@ -33,241 +33,296 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.logging.Logger +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.create + +abstract class VendorExtension { + /** Controls dead code elimination, enabled if true. */ + abstract val optimize: Property + + init { + optimize.convention(true) + } +} class VendorPlugin : Plugin { - override fun apply(project: Project) { - project.plugins.all { - when (this) { - is LibraryPlugin -> configureAndroid(project) - } - } + override fun apply(project: Project) { + val vendorConfig = project.extensions.create("vendor") + project.plugins.all { + when (this) { + is LibraryPlugin -> configureAndroid(project, vendorConfig) + } } + } - fun configureAndroid(project: Project) { - project.apply(plugin = "LicenseResolverPlugin") - - val vendor = project.configurations.create("vendor") - project.configurations.all { - when (name) { - "compileOnly", "testImplementation", "androidTestImplementation" -> extendsFrom(vendor) - } - } + fun configureAndroid(project: Project, vendorConfig: VendorExtension) { + project.apply(plugin = "LicenseResolverPlugin") - val jarJar = project.configurations.create("firebaseJarJarArtifact") - project.dependencies.add("firebaseJarJarArtifact", "org.pantsbuild:jarjar:1.7.2") - - val android = project.extensions.getByType(LibraryExtension::class.java) - - android.registerTransform(VendorTransform( - android, - vendor, - JarJarTransformer( - parentPackageProvider = { - android.libraryVariants.find { it.name == "release" }!!.applicationId - }, - jarJarProvider = { jarJar.resolve() }, - project = project, - logger = project.logger), - logger = project.logger)) + val vendor = project.configurations.create("vendor") + project.configurations.all { + when (name) { + "compileOnly", + "testImplementation", + "androidTestImplementation" -> extendsFrom(vendor) + } } + + val jarJar = project.configurations.create("firebaseJarJarArtifact") + project.dependencies.add("firebaseJarJarArtifact", "org.pantsbuild:jarjar:1.7.2") + + val android = project.extensions.getByType(LibraryExtension::class.java) + + android.registerTransform( + VendorTransform( + android, + vendor, + JarJarTransformer( + parentPackageProvider = { + android.libraryVariants.find { it.name == "release" }!!.applicationId + }, + jarJarProvider = { jarJar.resolve() }, + project = project, + logger = project.logger, + optimize = vendorConfig.optimize + ), + logger = project.logger + ) + ) + } } interface JarTransformer { - fun transform(inputJar: File, outputJar: File, packagesToVendor: Set) + fun transform( + inputJar: File, + outputJar: File, + ownPackages: Set, + packagesToVendor: Set + ) } class JarJarTransformer( - private val parentPackageProvider: () -> String, - private val jarJarProvider: () -> Collection, - private val project: Project, - private val logger: Logger + private val parentPackageProvider: () -> String, + private val jarJarProvider: () -> Collection, + private val project: Project, + private val logger: Logger, + private val optimize: Provider ) : JarTransformer { - override fun transform(inputJar: File, outputJar: File, packagesToVendor: Set) { - val parentPackage = parentPackageProvider() - val rulesFile = File.createTempFile(parentPackage, ".jarjar") - rulesFile.printWriter().use { - for (externalPackageName in packagesToVendor) { - it.println("rule $externalPackageName.** $parentPackage.@0") - } + override fun transform( + inputJar: File, + outputJar: File, + ownPackages: Set, + packagesToVendor: Set + ) { + val parentPackage = parentPackageProvider() + val rulesFile = File.createTempFile(parentPackage, ".jarjar") + rulesFile.printWriter().use { + if (optimize.get()) { + for (packageName in ownPackages) { + it.println("keep $packageName.**") } - logger.info("The following JarJar configuration will be used:\n ${rulesFile.readText()}") - - project.javaexec { - main = "org.pantsbuild.jarjar.Main" - classpath = project.files(jarJarProvider()) - args = listOf("process", rulesFile.absolutePath, inputJar.absolutePath, outputJar.absolutePath) - systemProperties = mapOf("verbose" to "true", "misplacedClassStrategy" to "FATAL") - }.assertNormalExitValue() + } + for (externalPackageName in packagesToVendor) { + it.println("rule $externalPackageName.** $parentPackage.@0") + } } + logger.info("The following JarJar configuration will be used:\n ${rulesFile.readText()}") + + project + .javaexec { + main = "org.pantsbuild.jarjar.Main" + classpath = project.files(jarJarProvider()) + args = + listOf("process", rulesFile.absolutePath, inputJar.absolutePath, outputJar.absolutePath) + systemProperties = mapOf("verbose" to "true", "misplacedClassStrategy" to "FATAL") + } + .assertNormalExitValue() + } } class VendorTransform( - private val android: LibraryExtension, - private val configuration: Configuration, - private val jarTransformer: JarTransformer, - private val logger: Logger -) : - Transform() { - override fun getName() = "firebaseVendorTransform" - - override fun getInputTypes(): MutableSet { - return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES) - } + private val android: LibraryExtension, + private val configuration: Configuration, + private val jarTransformer: JarTransformer, + private val logger: Logger +) : Transform() { + override fun getName() = "firebaseVendorTransform" - override fun isIncremental() = false + override fun getInputTypes(): MutableSet { + return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES) + } - override fun getScopes(): MutableSet { - return mutableSetOf(QualifiedContent.Scope.PROJECT) - } + override fun isIncremental() = false - override fun getReferencedScopes(): MutableSet { - return mutableSetOf(QualifiedContent.Scope.PROJECT) - } + override fun getScopes(): MutableSet { + return mutableSetOf(QualifiedContent.Scope.PROJECT) + } - override fun transform(transformInvocation: TransformInvocation) { - if (configuration.resolve().isEmpty()) { - logger.warn("Nothing to vendor. " + - "If you don't need vendor functionality please disable 'firebase-vendor' plugin. " + - "Otherwise use the 'vendor' configuration to add dependencies you want vendored in.") - for (input in transformInvocation.inputs) { - for (directoryInput in input.directoryInputs) { - val directoryOutput = transformInvocation.outputProvider.getContentLocation( - directoryInput.name, - setOf(QualifiedContent.DefaultContentType.CLASSES), - mutableSetOf(QualifiedContent.Scope.PROJECT), - Format.DIRECTORY) - directoryInput.file.copyRecursively(directoryOutput, overwrite = true) - } - for (jarInput in input.jarInputs) { - val jarOutput = transformInvocation.outputProvider.getContentLocation( - jarInput.name, - setOf(QualifiedContent.DefaultContentType.CLASSES), - mutableSetOf(QualifiedContent.Scope.PROJECT), - Format.JAR) - - jarInput.file.copyTo(jarOutput, overwrite = true) - } - } - return + override fun getReferencedScopes(): MutableSet { + return mutableSetOf(QualifiedContent.Scope.PROJECT) + } + + override fun transform(transformInvocation: TransformInvocation) { + if (configuration.resolve().isEmpty()) { + logger.warn( + "Nothing to vendor. " + + "If you don't need vendor functionality please disable 'firebase-vendor' plugin. " + + "Otherwise use the 'vendor' configuration to add dependencies you want vendored in." + ) + for (input in transformInvocation.inputs) { + for (directoryInput in input.directoryInputs) { + val directoryOutput = + transformInvocation.outputProvider.getContentLocation( + directoryInput.name, + setOf(QualifiedContent.DefaultContentType.CLASSES), + mutableSetOf(QualifiedContent.Scope.PROJECT), + Format.DIRECTORY + ) + directoryInput.file.copyRecursively(directoryOutput, overwrite = true) } + for (jarInput in input.jarInputs) { + val jarOutput = + transformInvocation.outputProvider.getContentLocation( + jarInput.name, + setOf(QualifiedContent.DefaultContentType.CLASSES), + mutableSetOf(QualifiedContent.Scope.PROJECT), + Format.JAR + ) - val contentLocation = transformInvocation.outputProvider.getContentLocation( - "sourceAndVendoredLibraries", - setOf(QualifiedContent.DefaultContentType.CLASSES), - mutableSetOf(QualifiedContent.Scope.PROJECT), - Format.DIRECTORY) - contentLocation.deleteRecursively() - contentLocation.mkdirs() - val tmpDir = File(contentLocation, "tmp") - tmpDir.mkdirs() - try { - val fatJar = process(tmpDir, transformInvocation) - unzipJar(fatJar, contentLocation) - } finally { - tmpDir.deleteRecursively() + jarInput.file.copyTo(jarOutput, overwrite = true) } + } + return } - private fun isTest(transformInvocation: TransformInvocation): Boolean { - return android.testVariants.find { it.name == transformInvocation.context.variantName } != null + val contentLocation = + transformInvocation.outputProvider.getContentLocation( + "sourceAndVendoredLibraries", + setOf(QualifiedContent.DefaultContentType.CLASSES), + mutableSetOf(QualifiedContent.Scope.PROJECT), + Format.DIRECTORY + ) + contentLocation.deleteRecursively() + contentLocation.mkdirs() + val tmpDir = File(contentLocation, "tmp") + tmpDir.mkdirs() + try { + val fatJar = process(tmpDir, transformInvocation) + unzipJar(fatJar, contentLocation) + } finally { + tmpDir.deleteRecursively() } + } - private fun process(workDir: File, transformInvocation: TransformInvocation): File { - transformInvocation.context.variantName - val unzippedDir = File(workDir, "unzipped") - val unzippedExcludedDir = File(workDir, "unzipped-excluded") - unzippedDir.mkdirs() - unzippedExcludedDir.mkdirs() + private fun isTest(transformInvocation: TransformInvocation): Boolean { + return android.testVariants.find { it.name == transformInvocation.context.variantName } != null + } - val externalCodeDir = if (isTest(transformInvocation)) unzippedExcludedDir else unzippedDir + private fun process(workDir: File, transformInvocation: TransformInvocation): File { + transformInvocation.context.variantName + val unzippedDir = File(workDir, "unzipped") + val unzippedExcludedDir = File(workDir, "unzipped-excluded") + unzippedDir.mkdirs() + unzippedExcludedDir.mkdirs() - for (input in transformInvocation.inputs) { - for (directoryInput in input.directoryInputs) { - directoryInput.file.copyRecursively(unzippedDir) - } - for (jarInput in input.jarInputs) { - unzipJar(jarInput.file, unzippedDir) - } - } + val externalCodeDir = if (isTest(transformInvocation)) unzippedExcludedDir else unzippedDir - val ownPackageNames = inferPackages(unzippedDir) + for (input in transformInvocation.inputs) { + for (directoryInput in input.directoryInputs) { + directoryInput.file.copyRecursively(unzippedDir) + } + for (jarInput in input.jarInputs) { + unzipJar(jarInput.file, unzippedDir) + } + } - for (jar in configuration.resolve()) { - unzipJar(jar, externalCodeDir) - } - val externalPackageNames = inferPackages(externalCodeDir) subtract ownPackageNames - val java = File(externalCodeDir, "java") - val javax = File(externalCodeDir, "javax") - if (java.exists() || javax.exists()) { - // JarJar unconditionally skips any classes whose package name starts with "java" or "javax". - throw GradleException("Vendoring java or javax packages is not supported. " + - "Please exclude one of the direct or transitive dependencies: \n" + - configuration.resolvedConfiguration.resolvedArtifacts.joinToString(separator = "\n")) - } - val jar = File(workDir, "intermediate.jar") - zipAll(unzippedDir, jar) - val outputJar = File(workDir, "output.jar") + val ownPackageNames = inferPackages(unzippedDir) - jarTransformer.transform(jar, outputJar, externalPackageNames) - return outputJar + for (jar in configuration.resolve()) { + unzipJar(jar, externalCodeDir) } - - private fun inferPackages(dir: File): Set { - return dir.walk().filter { it.name.endsWith(".class") }.map { it.parentFile.toRelativeString(dir).replace('/', '.') }.toSet() + val externalPackageNames = inferPackages(externalCodeDir) subtract ownPackageNames + val java = File(externalCodeDir, "java") + val javax = File(externalCodeDir, "javax") + if (java.exists() || javax.exists()) { + // JarJar unconditionally skips any classes whose package name starts with "java" or "javax". + throw GradleException( + "Vendoring java or javax packages is not supported. " + + "Please exclude one of the direct or transitive dependencies: \n" + + configuration.resolvedConfiguration.resolvedArtifacts.joinToString(separator = "\n") + ) } + val jar = File(workDir, "intermediate.jar") + zipAll(unzippedDir, jar) + val outputJar = File(workDir, "output.jar") + + jarTransformer.transform(jar, outputJar, ownPackageNames, externalPackageNames) + return outputJar + } + + private fun inferPackages(dir: File): Set { + return dir + .walk() + .filter { it.name.endsWith(".class") } + .map { it.parentFile.toRelativeString(dir).replace('/', '.') } + .toSet() + } } fun unzipJar(jar: File, directory: File) { - ZipFile(jar).use { zip -> - zip.entries().asSequence().filter { !it.isDirectory && !it.name.startsWith("META-INF") }.forEach { entry -> - zip.getInputStream(entry).use { input -> - val entryFile = File(directory, entry.name) - entryFile.parentFile.mkdirs() - entryFile.outputStream().use { output -> - input.copyTo(output) - } - } + ZipFile(jar).use { zip -> + zip + .entries() + .asSequence() + .filter { !it.isDirectory && !it.name.startsWith("META-INF") } + .forEach { entry -> + zip.getInputStream(entry).use { input -> + val entryFile = File(directory, entry.name) + entryFile.parentFile.mkdirs() + entryFile.outputStream().use { output -> input.copyTo(output) } } - } + } + } } fun zipAll(directory: File, zipFile: File) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { - zipFiles(it, directory, "") - } + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { + zipFiles(it, directory, "") + } } private fun zipFiles(zipOut: ZipOutputStream, sourceFile: File, parentDirPath: String) { - val data = ByteArray(2048) - sourceFile.listFiles()?.forEach { f -> - if (f.isDirectory) { - val path = if (parentDirPath == "") { - f.name - } else { - parentDirPath + File.separator + f.name - } - // Call recursively to add files within this directory - zipFiles(zipOut, f, path) + val data = ByteArray(2048) + sourceFile.listFiles()?.forEach { f -> + if (f.isDirectory) { + val path = + if (parentDirPath == "") { + f.name } else { - FileInputStream(f).use { fi -> - BufferedInputStream(fi).use { origin -> - val path = parentDirPath + File.separator + f.name - val entry = ZipEntry(path) - entry.time = f.lastModified() - entry.isDirectory - entry.size = f.length() - zipOut.putNextEntry(entry) - while (true) { - val readBytes = origin.read(data) - if (readBytes == -1) { - break - } - zipOut.write(data, 0, readBytes) - } - } + parentDirPath + File.separator + f.name + } + // Call recursively to add files within this directory + zipFiles(zipOut, f, path) + } else { + FileInputStream(f).use { fi -> + BufferedInputStream(fi).use { origin -> + val path = parentDirPath + File.separator + f.name + val entry = ZipEntry(path) + entry.time = f.lastModified() + entry.isDirectory + entry.size = f.length() + zipOut.putNextEntry(entry) + while (true) { + val readBytes = origin.read(data) + if (readBytes == -1) { + break } + zipOut.write(data, 0, readBytes) + } } + } } + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/ChangedModulesTask.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/ChangedModulesTask.kt index 7eeb706c483..4b5e8fa7335 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/ChangedModulesTask.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/ChangedModulesTask.kt @@ -1,5 +1,20 @@ +// Copyright 2022 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.gson.Gson import java.io.File import org.gradle.api.DefaultTask @@ -8,46 +23,59 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.findByType abstract class ChangedModulesTask : DefaultTask() { - @get:Input - @set:Option(option = "changed-git-paths", description = "Hellos") - abstract var changedGitPaths: List + @get:Input + @set:Option(option = "changed-git-paths", description = "The list of changed paths") + abstract var changedGitPaths: List - @get:Input - @set:Option(option = "output-file-path", description = "Hello") - abstract var outputFilePath: String + @get:Input + @set:Option(option = "output-file-path", description = "The file to output json to") + abstract var outputFilePath: String - @get:OutputFile - val outputFile by lazy { - File(outputFilePath) - } + @get:Input + @set:Option(option = "only-firebase-sdks", description = "Only list Firebase SDKs") + abstract var onlyFirebaseSDKs: Boolean - init { - outputs.upToDateWhen { false } - } + @get:OutputFile val outputFile by lazy { File(outputFilePath) } - @TaskAction - fun execute() { - val projects = - AffectedProjectFinder(project, changedGitPaths.toSet(), listOf()).find().map { it.path } - .toSet() + init { + outputs.upToDateWhen { false } + } - val result = project.rootProject.subprojects.associate { - it.path to mutableSetOf() + @TaskAction + fun execute() { + val projects = + AffectedProjectFinder(project, changedGitPaths.toSet(), listOf()) + .find() + .filter { + val ext = it.extensions.findByType(FirebaseLibraryExtension::class.java) + !onlyFirebaseSDKs || it.extensions.findByType() != null } - project.rootProject.subprojects.forEach { p -> - p.configurations.forEach { c -> - c.dependencies.filterIsInstance().forEach { - result[it.dependencyProject.path]?.add(p.path) - } + .map { it.path } + .toSet() + + val result = project.rootProject.subprojects.associate { it.path to mutableSetOf() } + project.rootProject.subprojects.forEach { p -> + p.configurations.forEach { c -> + c.dependencies.filterIsInstance().forEach { + if ( + !onlyFirebaseSDKs || + it.dependencyProject.extensions.findByType() != null + ) { + if (!onlyFirebaseSDKs || p.extensions.findByType() != null) { + result[it.dependencyProject.path]?.add(p.path) } + } } - val affectedProjects = - result.flatMap { (key, value) -> - if (projects.contains(key)) setOf(key) + value else setOf() - }.toSet() - - outputFile.writeText(Gson().toJson(affectedProjects)) + } } + val affectedProjects = + result + .flatMap { (key, value) -> if (projects.contains(key)) setOf(key) + value else setOf() } + .toSet() + + outputFile.writeText(Gson().toJson(affectedProjects)) + } } diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/Coverage.java b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/Coverage.java index bdf7df5bc1f..eaf60c2d06c 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/Coverage.java +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/Coverage.java @@ -35,7 +35,7 @@ public static void apply(FirebaseLibraryExtension firebaseLibrary) { File reportsDir = new File(project.getBuildDir(), "/reports/jacoco"); JacocoPluginExtension jacoco = project.getExtensions().getByType(JacocoPluginExtension.class); - jacoco.setToolVersion("0.8.5"); + jacoco.setToolVersion("0.8.8"); jacoco.setReportsDir(reportsDir); project .getTasks() diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/bomgenerator/tagging/GitClientTest.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/bomgenerator/tagging/GitClientTest.kt index a18438b6d78..7fe6b8d4f7d 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/bomgenerator/tagging/GitClientTest.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/bomgenerator/tagging/GitClientTest.kt @@ -27,72 +27,73 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class GitClientTest { - @Rule @JvmField val testGitDirectory = TemporaryFolder() - private val branch = AtomicReference() - private val commit = AtomicReference() + @Rule @JvmField val testGitDirectory = TemporaryFolder() + private val branch = AtomicReference() + private val commit = AtomicReference() - private lateinit var executor: ShellExecutor - private val handler: (List) -> Unit = { it.forEach(System.out::println) } + private lateinit var executor: ShellExecutor + private val handler: (List) -> Unit = { it.forEach(System.out::println) } - @Before - fun setup() { - testGitDirectory.newFile("hello.txt").writeText("hello git!") - executor = ShellExecutor(testGitDirectory.root, System.out::println) + @Before + fun setup() { + testGitDirectory.newFile("hello.txt").writeText("hello git!") + executor = ShellExecutor(testGitDirectory.root, System.out::println) - executor.execute("git init", handler) - executor.execute("git config user.email 'GitClientTest@example.com'", handler) - executor.execute("git config user.name 'GitClientTest'", handler) - executor.execute("git add .", handler) - executor.execute("git commit -m 'init_commit'", handler) - executor.execute("git status", handler) + executor.execute("git init", handler) + executor.execute("git config user.email 'GitClientTest@example.com'", handler) + executor.execute("git config user.name 'GitClientTest'", handler) + executor.execute("git add .", handler) + executor.execute("git commit -m 'init_commit'", handler) + executor.execute("git status", handler) - executor.execute("git rev-parse --abbrev-ref HEAD", handler) { branch.set(it[0]) } - executor.execute("git rev-parse HEAD", handler) { commit.set(it[0]) } - } + executor.execute("git rev-parse --abbrev-ref HEAD", handler) { branch.set(it[0]) } + executor.execute("git rev-parse HEAD", handler) { commit.set(it[0]) } + } - @Test - fun `tag M release version succeeds on local file system`() { - val git = GitClient(branch.get(), commit.get(), executor, System.out::println) - git.tagReleaseVersion() - executor.execute("git tag --points-at HEAD", handler) { - Assert.assertTrue(it.stream().anyMatch { x -> x.contains(branch.get()) }) - } + @Test + fun `tag M release version succeeds on local file system`() { + val git = GitClient(branch.get(), commit.get(), executor, System.out::println) + git.tagReleaseVersion() + executor.execute("git tag --points-at HEAD", handler) { + Assert.assertTrue(it.stream().anyMatch { x -> x.contains(branch.get()) }) } + } - @Test - fun `tag bom version succeeds on local file system`() { - val git = GitClient(branch.get(), commit.get(), executor, System.out::println) - git.tagBomVersion("1.2.3") - executor.execute("git tag --points-at HEAD", handler) { - Assert.assertTrue(it.stream().anyMatch { x -> x.contains("bom@1.2.3") }) - } + @Test + fun `tag bom version succeeds on local file system`() { + val git = GitClient(branch.get(), commit.get(), executor, System.out::println) + git.tagBomVersion("1.2.3") + executor.execute("git tag --points-at HEAD", handler) { + Assert.assertTrue(it.stream().anyMatch { x -> x.contains("bom@1.2.3") }) } + } - @Test - fun `tag product version succeeds on local file system`() { - val git = GitClient(branch.get(), commit.get(), executor, System.out::println) - git.tagProductVersion("firebase-database", "1.2.3") - executor.execute("git tag --points-at HEAD", handler) { - Assert.assertTrue(it.stream().anyMatch { x -> x.contains("firebase-database@1.2.3") }) - } + @Test + fun `tag product version succeeds on local file system`() { + val git = GitClient(branch.get(), commit.get(), executor, System.out::println) + git.tagProductVersion("firebase-database", "1.2.3") + executor.execute("git tag --points-at HEAD", handler) { + Assert.assertTrue(it.stream().anyMatch { x -> x.contains("firebase-database@1.2.3") }) } + } - @Test - fun `tags are pushed to the remote repository`() { - Assume.assumeTrue(System.getenv().containsKey("FIREBASE_CI")) + @Test + fun `tags are pushed to the remote repository`() { + Assume.assumeTrue(System.getenv().containsKey("FIREBASE_CI")) - val mockExecutor = object : ShellExecutor(testGitDirectory.root, System.out::println) { - override fun execute(command: String, consumer: Consumer>) { - consumer.accept(listOf("Received command: $command")) - } + val mockExecutor = + object : ShellExecutor(testGitDirectory.root, System.out::println) { + override fun execute(command: String, consumer: Consumer>) { + consumer.accept(listOf("Received command: $command")) } + } - val outputs = mutableListOf() - val git = GitClient(branch.get(), commit.get(), mockExecutor) { outputs.add(it) } - git.tagBomVersion("1.2.3") - git.tagProductVersion("firebase-functions", "1.2.3") - git.pushCreatedTags() + val outputs = mutableListOf() + val git = GitClient(branch.get(), commit.get(), mockExecutor) { outputs.add(it) } + git.tagBomVersion("1.2.3") + git.tagProductVersion("firebase-functions", "1.2.3") + git.pushCreatedTags() - Assert.assertTrue(outputs.stream().anyMatch { it.contains("git push origin") }) - } + Assert.assertTrue(outputs.stream().anyMatch { it.contains("git push origin") }) + } } diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/LicenseResolverPluginTests.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/LicenseResolverPluginTests.kt index 66b1014a818..14755fbf1f2 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/LicenseResolverPluginTests.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/LicenseResolverPluginTests.kt @@ -33,45 +33,46 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class LicenseResolverPluginTests { - @Rule - @JvmField - val testProjectDir = TemporaryFolder() - private lateinit var buildFile: File - - val idempotentBuild: (taskName: String) -> BuildResult - get() = this::build.memoize() - - @Before - fun setup() { - buildFile = testProjectDir.newFile("build.gradle") - testProjectDir.newFolder("src", "main", "java", "com", "example") - testProjectDir.newFile("src/main/java/com/example/Foo.java").writeText("package com.example; class Foo {}") - testProjectDir.newFile("src/main/AndroidManifest.xml").writeText(MANIFEST) - - buildFile.writeText(BUILD_CONFIG) - } - - @Test - fun `Generating licenses`() { - val result = idempotentBuild("generateLicenses") - assertThat(result.task(":generateLicenses")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val json = getLicenseJson() - val txt = getLicenseText() - - assertThat(txt).isNotEmpty() - - assertThat(json).containsKey("customLib1") - - assertThat(txt).contains("customLib1") - assertThat(txt).contains("Test license") - val (start, length) = json["customLib1"]!! - assertThat(txt.substring(start, start + length).trim()).isEqualTo("Test license") - } - - @Test - fun `License tasks throw useful exception if file URI not found`() { - buildFile.writeText(""" + @Rule @JvmField val testProjectDir = TemporaryFolder() + private lateinit var buildFile: File + + val idempotentBuild: (taskName: String) -> BuildResult + get() = this::build.memoize() + + @Before + fun setup() { + buildFile = testProjectDir.newFile("build.gradle") + testProjectDir.newFolder("src", "main", "java", "com", "example") + testProjectDir + .newFile("src/main/java/com/example/Foo.java") + .writeText("package com.example; class Foo {}") + testProjectDir.newFile("src/main/AndroidManifest.xml").writeText(MANIFEST) + + buildFile.writeText(BUILD_CONFIG) + } + + @Test + fun `Generating licenses`() { + val result = idempotentBuild("generateLicenses") + assertThat(result.task(":generateLicenses")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val json = getLicenseJson() + val txt = getLicenseText() + + assertThat(txt).isNotEmpty() + + assertThat(json).containsKey("customLib1") + + assertThat(txt).contains("customLib1") + assertThat(txt).contains("Test license") + val (start, length) = json["customLib1"]!! + assertThat(txt.substring(start, start + length).trim()).isEqualTo("Test license") + } + + @Test + fun `License tasks throw useful exception if file URI not found`() { + buildFile.writeText( + """ plugins { id 'com.android.library' id 'LicenseResolverPlugin' @@ -81,41 +82,49 @@ class LicenseResolverPluginTests { thirdPartyLicenses { add 'customLib', "${File("non_existent_path.txt").absolutePath}" } - """) - - val thrown = Assert.assertThrows(UnexpectedBuildFailure::class.java) { - build("generateLicenses") - } - - assertThat(thrown.message).contains("License file not found") - } - - data class FileOffset(val start: Int, val length: Int) - - private fun getLicenseJson(): Map = - Gson().fromJson( - File("${testProjectDir.root}/build/generated/third_party_licenses/", - "third_party_licenses.json").readText(), - object : TypeToken>() {}.type) - - private fun getLicenseText(): String = - File("${testProjectDir.root}/build/generated/third_party_licenses/", - "third_party_licenses.txt").readText() - - private fun build(taskName: String): BuildResult = GradleRunner.create() - .withProjectDir(testProjectDir.root) - .withArguments(taskName, "--stacktrace") - .withPluginClasspath() - .build() - - companion object { - const val MANIFEST = """ + """ + ) + + val thrown = + Assert.assertThrows(UnexpectedBuildFailure::class.java) { build("generateLicenses") } + + assertThat(thrown.message).contains("License file not found") + } + + data class FileOffset(val start: Int, val length: Int) + + private fun getLicenseJson(): Map = + Gson() + .fromJson( + File( + "${testProjectDir.root}/build/generated/third_party_licenses/", + "third_party_licenses.json" + ) + .readText(), + object : TypeToken>() {}.type + ) + + private fun getLicenseText(): String = + File("${testProjectDir.root}/build/generated/third_party_licenses/", "third_party_licenses.txt") + .readText() + + private fun build(taskName: String): BuildResult = + GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments(taskName, "--stacktrace") + .withPluginClasspath() + .build() + + companion object { + const val MANIFEST = + """ """ - val BUILD_CONFIG = """ + val BUILD_CONFIG = + """ buildscript { repositories { google() @@ -144,5 +153,5 @@ class LicenseResolverPluginTests { add 'customLib1', "${File("src/test/fixtures/license.txt").absolutePath}" } """ - } + } } diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/Memoization.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/Memoization.kt index 7d25f4f835e..d3c69b18b28 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/Memoization.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/Memoization.kt @@ -15,10 +15,10 @@ package com.google.firebase.gradle.plugins class Memoize1(val f: (T) -> R) : (T) -> R { - private val values = mutableMapOf() - override fun invoke(x: T): R { - return values.getOrPut(x, { f(x) }) - } + private val values = mutableMapOf() + override fun invoke(x: T): R { + return values.getOrPut(x, { f(x) }) + } } fun ((T) -> R).memoize(): (T) -> R = Memoize1(this) diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/PublishingPluginTests.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/PublishingPluginTests.kt index c4461c45b41..32d4aae45c6 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/PublishingPluginTests.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/PublishingPluginTests.kt @@ -27,311 +27,345 @@ import org.junit.Test import org.junit.rules.TemporaryFolder class PublishingPluginTests { - @Rule - @JvmField - val testProjectDir = TemporaryFolder() - - private val subprojects = mutableListOf() - private lateinit var rootBuildFile: File - private lateinit var rootSettingsFile: File - - @Test - fun `Publishing dependent projects succeeds`() { - val project1 = Project(name = "childProject1", version = "1.0") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1), - customizePom = """ + @Rule @JvmField val testProjectDir = TemporaryFolder() + + private val subprojects = mutableListOf() + private lateinit var rootBuildFile: File + private lateinit var rootSettingsFile: File + + @Test + fun `Publishing dependent projects succeeds`() { + val project1 = Project(name = "childProject1", version = "1.0") + val project2 = + Project( + name = "childProject2", + version = "0.9", + projectDependencies = setOf(project1), + customizePom = """ licenses { license { name = 'Hello' } } -""") - subprojectsDefined(project1, project2) - val result = publish(Mode.RELEASE, project1, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNotNull() - assertThat(pomOrNull2).isNotNull() - val pom1 = pomOrNull1!! - val pom2 = pomOrNull2!! - - assertThat(pom1.artifact.version).isEqualTo(project1.version) - assertThat(pom2.artifact.version).isEqualTo(project2.version) - assertThat(pom1.license).isEqualTo(License( - "The Apache Software License, Version 2.0", - "http://www.apache.org/licenses/LICENSE-2.0.txt")) - assertThat(pom2.license).isEqualTo(License( - "Hello", - "")) - - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.version, - type = Type.AAR, - scope = "compile"))) - } - - @Test - fun `Publishing dependent projects one of which is a jar succeeds`() { - val project1 = Project(name = "childProject1", version = "1.0", libraryType = LibraryType.JAVA) - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1), - customizePom = """ +""" + ) + subprojectsDefined(project1, project2) + val result = publish(Mode.RELEASE, project1, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNotNull() + assertThat(pomOrNull2).isNotNull() + val pom1 = pomOrNull1!! + val pom2 = pomOrNull2!! + + assertThat(pom1.artifact.version).isEqualTo(project1.version) + assertThat(pom2.artifact.version).isEqualTo(project2.version) + assertThat(pom1.license) + .isEqualTo( + License( + "The Apache Software License, Version 2.0", + "http://www.apache.org/licenses/LICENSE-2.0.txt" + ) + ) + assertThat(pom2.license).isEqualTo(License("Hello", "")) + + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.version, + type = Type.AAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publishing dependent projects one of which is a jar succeeds`() { + val project1 = Project(name = "childProject1", version = "1.0", libraryType = LibraryType.JAVA) + val project2 = + Project( + name = "childProject2", + version = "0.9", + projectDependencies = setOf(project1), + customizePom = """ licenses { license { name = 'Hello' } } -""") - subprojectsDefined(project1, project2) - val result = publish(Mode.RELEASE, project1, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNotNull() - assertThat(pomOrNull2).isNotNull() - val pom1 = pomOrNull1!! - val pom2 = pomOrNull2!! - - assertThat(pom1.artifact.version).isEqualTo(project1.version) - assertThat(pom2.artifact.version).isEqualTo(project2.version) - assertThat(pom1.license).isEqualTo(License( - "The Apache Software License, Version 2.0", - "http://www.apache.org/licenses/LICENSE-2.0.txt")) - assertThat(pom2.license).isEqualTo(License( - "Hello", - "")) - - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.version, - type = Type.JAR, - scope = "compile"))) - } - - @Test - fun `Publish with unreleased dependency`() { - val project1 = Project(name = "childProject1", version = "1.0") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1)) - - subprojectsDefined(project1, project2) - val exception = Assert.assertThrows(UnexpectedBuildFailure::class.java) { - publish(Mode.RELEASE, project2) - } - assertThat(exception.message).contains("Failed to release com.example:childProject2") - } - - @Test - fun `Publish with released dependency`() { - val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1)) - subprojectsDefined(project1, project2) - - val result = publish(Mode.RELEASE, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNull() - assertThat(pomOrNull2).isNotNull() - - val pom2 = pomOrNull2!! - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.latestReleasedVersion!!, - type = Type.AAR, - scope = "compile"))) - } - - @Test - fun `Publish all dependent snapshot projects succeeds`() { - val project1 = Project(name = "childProject1", version = "1.0") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1)) - subprojectsDefined(project1, project2) - val result = publish(Mode.SNAPSHOT, project1, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNotNull() - assertThat(pomOrNull2).isNotNull() - - val pom1 = pomOrNull1!! - val pom2 = pomOrNull2!! - - assertThat(pom1.artifact.version).isEqualTo("${project1.version}-SNAPSHOT") - assertThat(pom2.artifact.version).isEqualTo("${project2.version}-SNAPSHOT") - - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = "${project1.version}-SNAPSHOT", - type = Type.AAR, - scope = "compile"))) - } - - @Test - fun `Publish snapshots with released dependency`() { - val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1)) - subprojectsDefined(project1, project2) - - val result = publish(Mode.SNAPSHOT, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNull() - assertThat(pomOrNull2).isNotNull() - - val pom2 = pomOrNull2!! - - assertThat(pom2.artifact.version).isEqualTo("${project2.version}-SNAPSHOT") - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.latestReleasedVersion!!, - type = Type.AAR, - scope = "compile"))) - } - - @Test - fun `Publish project should also publish coreleased projects`() { - val project1 = Project(name = "childProject1", version = "1.0") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1), - releaseWith = project1) - subprojectsDefined(project1, project2) - - val result = publish(Mode.RELEASE, project1) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull1).isNotNull() - assertThat(pomOrNull2).isNotNull() - - val pom1 = pomOrNull1!! - val pom2 = pomOrNull2!! - - assertThat(pom1.artifact.version).isEqualTo(project1.version) - assertThat(pom2.artifact.version).isEqualTo(project1.version) - assertThat(pom2.dependencies).isEqualTo( - listOf(Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.version, - type = Type.AAR, - scope = "compile"))) - } - - @Test - fun `Publish project should correctly set dependency types`() { - val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") - val project2 = Project( - name = "childProject2", - version = "0.9", - projectDependencies = setOf(project1), - externalDependencies = setOf( - Artifact("com.google.dagger", "dagger", "2.22"), - Artifact("com.google.dagger", "dagger-android-support", "2.22"), - Artifact("com.android.support", "multidex", "1.0.3") - )) - subprojectsDefined(project1, project2) - - val result = publish(Mode.RELEASE, project2) - assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) - - val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") - assertThat(pomOrNull2).isNotNull() - - val pom2 = pomOrNull2!! - assertThat(pom2.artifact.version).isEqualTo(project2.version) - assertThat(pom2.dependencies).containsExactly( - Artifact( - groupId = project1.group, - artifactId = project1.name, - version = project1.latestReleasedVersion!!, - type = Type.AAR, - scope = "compile"), - Artifact( - groupId = "com.google.dagger", - artifactId = "dagger", - version = "2.22", - type = Type.JAR, - scope = "compile"), - Artifact( - groupId = "com.google.dagger", - artifactId = "dagger-android-support", - version = "2.22", - type = Type.AAR, - scope = "compile") +""" + ) + subprojectsDefined(project1, project2) + val result = publish(Mode.RELEASE, project1, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNotNull() + assertThat(pomOrNull2).isNotNull() + val pom1 = pomOrNull1!! + val pom2 = pomOrNull2!! + + assertThat(pom1.artifact.version).isEqualTo(project1.version) + assertThat(pom2.artifact.version).isEqualTo(project2.version) + assertThat(pom1.license) + .isEqualTo( + License( + "The Apache Software License, Version 2.0", + "http://www.apache.org/licenses/LICENSE-2.0.txt" ) - } - - private fun publish(mode: Mode, vararg projects: Project): BuildResult = - GradleRunner.create() - .withProjectDir(testProjectDir.root) - .withArguments( - "-PprojectsToPublish=${projects.joinToString(",") { it.name }}", - "-PpublishMode=$mode", - "firebasePublish") - .withPluginClasspath() - .build() - - private fun include(project: Project) { - testProjectDir.newFolder(project.name, "src", "main") - testProjectDir - .newFile("${project.name}/build.gradle") - .writeText(project.generateBuildFile()) - testProjectDir - .newFile("${project.name}/src/main/AndroidManifest.xml") - .writeText(MANIFEST) - subprojects.add(project) - } - - private fun subprojectsDefined(vararg projects: Project) { - rootBuildFile = testProjectDir.newFile("build.gradle") - rootSettingsFile = testProjectDir.newFile("settings.gradle") - - projects.forEach(this::include) - - rootBuildFile.writeText(ROOT_PROJECT) - rootSettingsFile.writeText(projects.joinToString("\n") { "include ':${it.name}'" }) - } - - companion object { - private const val ROOT_PROJECT = """ + ) + assertThat(pom2.license).isEqualTo(License("Hello", "")) + + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.version, + type = Type.JAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publish with unreleased dependency`() { + val project1 = Project(name = "childProject1", version = "1.0") + val project2 = + Project(name = "childProject2", version = "0.9", projectDependencies = setOf(project1)) + + subprojectsDefined(project1, project2) + val exception = + Assert.assertThrows(UnexpectedBuildFailure::class.java) { publish(Mode.RELEASE, project2) } + assertThat(exception.message).contains("Failed to release com.example:childProject2") + } + + @Test + fun `Publish with released dependency`() { + val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") + val project2 = + Project(name = "childProject2", version = "0.9", projectDependencies = setOf(project1)) + subprojectsDefined(project1, project2) + + val result = publish(Mode.RELEASE, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNull() + assertThat(pomOrNull2).isNotNull() + + val pom2 = pomOrNull2!! + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.latestReleasedVersion!!, + type = Type.AAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publish all dependent snapshot projects succeeds`() { + val project1 = Project(name = "childProject1", version = "1.0") + val project2 = + Project(name = "childProject2", version = "0.9", projectDependencies = setOf(project1)) + subprojectsDefined(project1, project2) + val result = publish(Mode.SNAPSHOT, project1, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNotNull() + assertThat(pomOrNull2).isNotNull() + + val pom1 = pomOrNull1!! + val pom2 = pomOrNull2!! + + assertThat(pom1.artifact.version).isEqualTo("${project1.version}-SNAPSHOT") + assertThat(pom2.artifact.version).isEqualTo("${project2.version}-SNAPSHOT") + + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = "${project1.version}-SNAPSHOT", + type = Type.AAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publish snapshots with released dependency`() { + val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") + val project2 = + Project(name = "childProject2", version = "0.9", projectDependencies = setOf(project1)) + subprojectsDefined(project1, project2) + + val result = publish(Mode.SNAPSHOT, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNull() + assertThat(pomOrNull2).isNotNull() + + val pom2 = pomOrNull2!! + + assertThat(pom2.artifact.version).isEqualTo("${project2.version}-SNAPSHOT") + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.latestReleasedVersion!!, + type = Type.AAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publish project should also publish coreleased projects`() { + val project1 = Project(name = "childProject1", version = "1.0") + val project2 = + Project( + name = "childProject2", + version = "0.9", + projectDependencies = setOf(project1), + releaseWith = project1 + ) + subprojectsDefined(project1, project2) + + val result = publish(Mode.RELEASE, project1) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull1 = project1.getPublishedPom("${testProjectDir.root}/build/m2repository") + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull1).isNotNull() + assertThat(pomOrNull2).isNotNull() + + val pom1 = pomOrNull1!! + val pom2 = pomOrNull2!! + + assertThat(pom1.artifact.version).isEqualTo(project1.version) + assertThat(pom2.artifact.version).isEqualTo(project1.version) + assertThat(pom2.dependencies) + .isEqualTo( + listOf( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.version, + type = Type.AAR, + scope = "compile" + ) + ) + ) + } + + @Test + fun `Publish project should correctly set dependency types`() { + val project1 = Project(name = "childProject1", version = "1.0", latestReleasedVersion = "0.8") + val project2 = + Project( + name = "childProject2", + version = "0.9", + projectDependencies = setOf(project1), + externalDependencies = + setOf( + Artifact("com.google.dagger", "dagger", "2.22"), + Artifact("com.google.dagger", "dagger-android-support", "2.22"), + Artifact("com.android.support", "multidex", "1.0.3") + ) + ) + subprojectsDefined(project1, project2) + + val result = publish(Mode.RELEASE, project2) + assertThat(result.task(":firebasePublish")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + val pomOrNull2 = project2.getPublishedPom("${testProjectDir.root}/build/m2repository") + assertThat(pomOrNull2).isNotNull() + + val pom2 = pomOrNull2!! + assertThat(pom2.artifact.version).isEqualTo(project2.version) + assertThat(pom2.dependencies) + .containsExactly( + Artifact( + groupId = project1.group, + artifactId = project1.name, + version = project1.latestReleasedVersion!!, + type = Type.AAR, + scope = "compile" + ), + Artifact( + groupId = "com.google.dagger", + artifactId = "dagger", + version = "2.22", + type = Type.JAR, + scope = "compile" + ), + Artifact( + groupId = "com.google.dagger", + artifactId = "dagger-android-support", + version = "2.22", + type = Type.AAR, + scope = "compile" + ) + ) + } + + private fun publish(mode: Mode, vararg projects: Project): BuildResult = + GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments( + "-PprojectsToPublish=${projects.joinToString(",") { it.name }}", + "-PpublishMode=$mode", + "firebasePublish" + ) + .withPluginClasspath() + .build() + + private fun include(project: Project) { + testProjectDir.newFolder(project.name, "src", "main") + testProjectDir.newFile("${project.name}/build.gradle").writeText(project.generateBuildFile()) + testProjectDir.newFile("${project.name}/src/main/AndroidManifest.xml").writeText(MANIFEST) + subprojects.add(project) + } + + private fun subprojectsDefined(vararg projects: Project) { + rootBuildFile = testProjectDir.newFile("build.gradle") + rootSettingsFile = testProjectDir.newFile("settings.gradle") + + projects.forEach(this::include) + + rootBuildFile.writeText(ROOT_PROJECT) + rootSettingsFile.writeText(projects.joinToString("\n") { "include ':${it.name}'" }) + } + + companion object { + private const val ROOT_PROJECT = + """ buildscript { repositories { google() @@ -361,11 +395,12 @@ licenses { } } """ - private const val MANIFEST = """ + private const val MANIFEST = + """ """ - } + } } diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/VendorTests.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/VendorTests.kt index 632d36dc5f0..de722c12f75 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/VendorTests.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/VendorTests.kt @@ -28,7 +28,8 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.JUnit4 -private const val MANIFEST = """ +private const val MANIFEST = + """ @@ -37,27 +38,32 @@ private const val MANIFEST = """ @RunWith(JUnit4::class) class VendorPluginTests { - @Rule - @JvmField - val testProjectDir = TemporaryFolder() - - @Test - fun `vendor guava not excluding javax transitive deps should fail`() { - val buildFailure = assertThrows(UnexpectedBuildFailure::class.java) { - buildWith("vendor 'com.google.guava:guava:29.0-android'") - } - - assertThat(buildFailure.message).contains("Vendoring java or javax packages is not supported.") - } - - @Test - fun `vendor guava excluding javax transitive deps should include subset of guava`() { - val classes = buildWith(""" + @Rule @JvmField val testProjectDir = TemporaryFolder() + + @Test + fun `vendor guava not excluding javax transitive deps should fail`() { + val buildFailure = + assertThrows(UnexpectedBuildFailure::class.java) { + buildWith("vendor 'com.google.guava:guava:29.0-android'") + } + + assertThat(buildFailure.message).contains("Vendoring java or javax packages is not supported.") + } + + @Test + fun `vendor guava excluding javax transitive deps should include subset of guava`() { + val classes = + buildWith( + """ vendor('com.google.guava:guava:29.0-android') { exclude group: 'com.google.code.findbugs', module: 'jsr305' } - """.trimIndent(), - SourceFile(name = "com/example/Hello.java", content = """ + """ + .trimIndent(), + SourceFile( + name = "com/example/Hello.java", + content = + """ package com.example; import com.google.common.base.Preconditions; @@ -67,46 +73,63 @@ class VendorPluginTests { Preconditions.checkNotNull(args); } } - """.trimIndent())) - // expected to vendor preconditions and errorprone annotations from transitive dep. - assertThat(classes).containsAtLeast( - "com/example/Hello.class", - "com/example/com/google/common/base/Preconditions.class", - "com/example/com/google/errorprone/annotations/CanIgnoreReturnValue.class") - - // ImmutableList is not used, so it should be stripped out. - assertThat(classes).doesNotContain("com/google/common/collect/ImmutableList.class") - } - - @Test - fun `vendor dagger excluding javax transitive deps and not using it should include dagger`() { - val classes = buildWith(""" + """ + .trimIndent() + ) + ) + // expected to vendor preconditions and errorprone annotations from transitive dep. + assertThat(classes) + .containsAtLeast( + "com/example/Hello.class", + "com/example/com/google/common/base/Preconditions.class", + "com/example/com/google/errorprone/annotations/CanIgnoreReturnValue.class" + ) + + // ImmutableList is not used, so it should be stripped out. + assertThat(classes).doesNotContain("com/example/com/google/common/collect/ImmutableList.class") + } + + @Test + fun `vendor dagger excluding javax transitive deps and not using it should not include dagger`() { + val classes = + buildWith( + """ vendor ('com.google.dagger:dagger:2.27') { exclude group: "javax.inject", module: "javax.inject" } - """.trimIndent(), - SourceFile(name = "com/example/Hello.java", content = """ + """ + .trimIndent(), + SourceFile( + name = "com/example/Hello.java", + content = + """ package com.example; public class Hello { public static void main(String[] args) {} } - """.trimIndent())) - // expected classes - assertThat(classes).containsAtLeast( - "com/example/Hello.class", - "com/example/BuildConfig.class", - "com/example/dagger/Lazy.class") - } - - @Test - fun `vendor dagger excluding javax transitive deps should include dagger`() { - val classes = buildWith(""" + """ + .trimIndent() + ) + ) + // expected classes + assertThat(classes).containsExactly("com/example/Hello.class", "com/example/BuildConfig.class") + } + + @Test + fun `vendor dagger excluding javax transitive deps should include dagger`() { + val classes = + buildWith( + """ vendor ('com.google.dagger:dagger:2.27') { exclude group: "javax.inject", module: "javax.inject" } - """.trimIndent(), - SourceFile(name = "com/example/Hello.java", content = """ + """ + .trimIndent(), + SourceFile( + name = "com/example/Hello.java", + content = + """ package com.example; import dagger.Module; @@ -115,16 +138,24 @@ class VendorPluginTests { public class Hello { public static void main(String[] args) {} } - """.trimIndent())) - // expected classes - assertThat(classes).containsAtLeast( - "com/example/Hello.class", - "com/example/BuildConfig.class", - "com/example/dagger/Module.class") - } - - private fun buildWith(deps: String, vararg files: SourceFile): List { - testProjectDir.newFile("build.gradle").writeText(""" + """ + .trimIndent() + ) + ) + // expected classes + assertThat(classes) + .containsAtLeast( + "com/example/Hello.class", + "com/example/BuildConfig.class", + "com/example/dagger/Module.class" + ) + } + + private fun buildWith(deps: String, vararg files: SourceFile): List { + testProjectDir + .newFile("build.gradle") + .writeText( + """ buildscript { repositories { google() @@ -145,46 +176,43 @@ class VendorPluginTests { dependencies { $deps } - """.trimIndent()) - testProjectDir.newFile("settings.gradle") - .writeText("rootProject.name = 'testlib'") - - testProjectDir.newFolder("src/main/java") - testProjectDir - .newFile("src/main/AndroidManifest.xml") - .writeText(MANIFEST) - - for (file in files) { - // if (1+1 == 2) {throw RuntimeException("src/main/java/${Paths.get(file.name).parent}")} - testProjectDir.newFolder("src/main/java/${Paths.get(file.name).parent}") - testProjectDir.newFile("src/main/java/${file.name}").writeText(file.content) - } + """ + .trimIndent() + ) + testProjectDir.newFile("settings.gradle").writeText("rootProject.name = 'testlib'") - GradleRunner.create() - .withArguments("assemble") - .withProjectDir(testProjectDir.root) - .withPluginClasspath() - .build() - - val aarFile = File(testProjectDir.root, "build/outputs/aar/testlib-release.aar") - assertThat(aarFile.exists()).isTrue() - - val zipFile = ZipFile(aarFile) - val classesJar = zipFile.entries().asSequence() - .filter { it.name == "classes.jar" } - .first() - return ZipInputStream(zipFile.getInputStream(classesJar)).use { - val entries = mutableListOf() - var currentEntry = it.nextEntry - while (currentEntry != null) { - if (!currentEntry.isDirectory) { - entries.add(currentEntry.name) - } - currentEntry = it.nextEntry - } - entries + testProjectDir.newFolder("src/main/java") + testProjectDir.newFile("src/main/AndroidManifest.xml").writeText(MANIFEST) + + for (file in files) { + // if (1+1 == 2) {throw RuntimeException("src/main/java/${Paths.get(file.name).parent}")} + testProjectDir.newFolder("src/main/java/${Paths.get(file.name).parent}") + testProjectDir.newFile("src/main/java/${file.name}").writeText(file.content) + } + + GradleRunner.create() + .withArguments("assemble") + .withProjectDir(testProjectDir.root) + .withPluginClasspath() + .build() + + val aarFile = File(testProjectDir.root, "build/outputs/aar/testlib-release.aar") + assertThat(aarFile.exists()).isTrue() + + val zipFile = ZipFile(aarFile) + val classesJar = zipFile.entries().asSequence().filter { it.name == "classes.jar" }.first() + return ZipInputStream(zipFile.getInputStream(classesJar)).use { + val entries = mutableListOf() + var currentEntry = it.nextEntry + while (currentEntry != null) { + if (!currentEntry.isDirectory) { + entries.add(currentEntry.name) } + currentEntry = it.nextEntry + } + entries } + } } data class SourceFile(val name: String, val content: String) diff --git a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/publishing.kt b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/publishing.kt index 92b69b844c5..2b283cc0b46 100644 --- a/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/publishing.kt +++ b/buildSrc/src/test/kotlin/com/google/firebase/gradle/plugins/publishing.kt @@ -21,19 +21,19 @@ import org.w3c.dom.Element import org.w3c.dom.NodeList data class Project( - val name: String, - val group: String = "com.example", - val version: String = "undefined", - val latestReleasedVersion: String? = null, - val projectDependencies: Set = setOf(), - val externalDependencies: Set = setOf(), - val releaseWith: Project? = null, - val customizePom: String? = null, - val publishJavadoc: Boolean = false, - val libraryType: LibraryType = LibraryType.ANDROID + val name: String, + val group: String = "com.example", + val version: String = "undefined", + val latestReleasedVersion: String? = null, + val projectDependencies: Set = setOf(), + val externalDependencies: Set = setOf(), + val releaseWith: Project? = null, + val customizePom: String? = null, + val publishJavadoc: Boolean = false, + val libraryType: LibraryType = LibraryType.ANDROID ) { - fun generateBuildFile(): String { - return """ + fun generateBuildFile(): String { + return """ plugins { id 'firebase-${if (libraryType == LibraryType.JAVA) "java-" else ""}library' } @@ -52,147 +52,150 @@ data class Project( ${externalDependencies.joinToString("\n") { "implementation '${it.simpleDepString}'" }} } """ - } - - fun getPublishedPom(rootDirectory: String): Pom? { - val v = releaseWith?.version ?: version - return File(rootDirectory).walk().asSequence() - .filter { it.isFile } - .filter { - it.path.matches(Regex(".*/${group.replace('.', '/')}/$name/$v.*/.*\\.pom$")) - } - .map(Pom::parse) - .firstOrNull() - } + } + + fun getPublishedPom(rootDirectory: String): Pom? { + val v = releaseWith?.version ?: version + return File(rootDirectory) + .walk() + .asSequence() + .filter { it.isFile } + .filter { it.path.matches(Regex(".*/${group.replace('.', '/')}/$name/$v.*/.*\\.pom$")) } + .map(Pom::parse) + .firstOrNull() + } } data class License(val name: String, val url: String) enum class Type { - JAR, AAR + JAR, + AAR } data class Artifact( - val groupId: String, - val artifactId: String, - val version: String, - val type: Type = Type.JAR, - val scope: String = "" + val groupId: String, + val artifactId: String, + val version: String, + val type: Type = Type.JAR, + val scope: String = "" ) { - val simpleDepString: String - get() = "$groupId:$artifactId:$version" + val simpleDepString: String + get() = "$groupId:$artifactId:$version" } data class Pom( - val artifact: Artifact, - val license: License = License( - name = "The Apache Software License, Version 2.0", - url = "http://www.apache.org/licenses/LICENSE-2.0.txt"), - val dependencies: List = listOf() + val artifact: Artifact, + val license: License = + License( + name = "The Apache Software License, Version 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + ), + val dependencies: List = listOf() ) { - companion object { - fun parse(file: File): Pom { - val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) - val childNodes = document.documentElement.childNodes - - var groupId: String? = null - var artifactId: String? = null - var version: String? = null - var type = Type.JAR - var license: License? = null - var deps: List = listOf() - - for (i in 0 until childNodes.length) { - val child = childNodes.item(i) - if (child !is Element) { - continue - } - when (child.tagName) { - "groupId" -> groupId = child.textContent.trim() - "artifactId" -> artifactId = child.textContent.trim() - "version" -> version = child.textContent.trim() - "packaging" -> type = Type.valueOf(child.textContent.trim().toUpperCase()) - "licenses" -> license = parseLicense(child.getElementsByTagName("license")) - "dependencies" -> deps = parseDeps(child.getElementsByTagName("dependency")) - } - } - if (groupId == null) { - throw GradleException("'' missing in pom") - } - if (artifactId == null) { - throw GradleException("'' missing in pom") - } - if (version == null) { - throw GradleException("'' missing in pom") - } - if (license == null) { - throw GradleException("'' missing in pom") - } - - return Pom(Artifact(groupId, artifactId, version, type), license, deps) + companion object { + fun parse(file: File): Pom { + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) + val childNodes = document.documentElement.childNodes + + var groupId: String? = null + var artifactId: String? = null + var version: String? = null + var type = Type.JAR + var license: License? = null + var deps: List = listOf() + + for (i in 0 until childNodes.length) { + val child = childNodes.item(i) + if (child !is Element) { + continue } - - private fun parseDeps(nodes: NodeList): List { - val deps = mutableListOf() - for (i in 0 until nodes.length) { - val child = nodes.item(i) - if (child !is Element) { - continue - } - deps.add(parseDep(child)) - } - return deps + when (child.tagName) { + "groupId" -> groupId = child.textContent.trim() + "artifactId" -> artifactId = child.textContent.trim() + "version" -> version = child.textContent.trim() + "packaging" -> type = Type.valueOf(child.textContent.trim().toUpperCase()) + "licenses" -> license = parseLicense(child.getElementsByTagName("license")) + "dependencies" -> deps = parseDeps(child.getElementsByTagName("dependency")) } + } + if (groupId == null) { + throw GradleException("'' missing in pom") + } + if (artifactId == null) { + throw GradleException("'' missing in pom") + } + if (version == null) { + throw GradleException("'' missing in pom") + } + if (license == null) { + throw GradleException("'' missing in pom") + } + + return Pom(Artifact(groupId, artifactId, version, type), license, deps) + } - private fun parseDep(dependencies: Element): Artifact { - var groupId: String? = null - var artifactId: String? = null - var version: String? = null - var type = Type.JAR - var scope: String? = null - - val nodes = dependencies.childNodes - - for (i in 0 until nodes.length) { - val child = nodes.item(i) - if (child !is Element) { - continue - } - when (child.tagName) { - "groupId" -> groupId = child.textContent.trim() - "artifactId" -> artifactId = child.textContent.trim() - "version" -> version = child.textContent.trim() - "type" -> type = Type.valueOf(child.textContent.trim().toUpperCase()) - "scope" -> scope = child.textContent.trim() - } - } - if (groupId == null) { - throw GradleException("'' missing in pom") - } - if (artifactId == null) { - throw GradleException("'' missing in pom") - } - if (version == null) { - throw GradleException("'' missing in pom") - } - if (scope == null) { - throw GradleException("'' missing in pom") - } - - return Artifact(groupId, artifactId, version, type, scope) + private fun parseDeps(nodes: NodeList): List { + val deps = mutableListOf() + for (i in 0 until nodes.length) { + val child = nodes.item(i) + if (child !is Element) { + continue } + deps.add(parseDep(child)) + } + return deps + } - private fun parseLicense(nodes: NodeList): License? { - if (nodes.length == 0) { - return null - } - val license = nodes.item(0) as Element - val urlElements = license.getElementsByTagName("url") - val url = if (urlElements.length == 0) "" else urlElements.item(0).textContent.trim() + private fun parseDep(dependencies: Element): Artifact { + var groupId: String? = null + var artifactId: String? = null + var version: String? = null + var type = Type.JAR + var scope: String? = null + + val nodes = dependencies.childNodes - val nameElements = license.getElementsByTagName("name") - val name = if (nameElements.length == 0) "" else nameElements.item(0).textContent.trim() - return License(name = name, url = url) + for (i in 0 until nodes.length) { + val child = nodes.item(i) + if (child !is Element) { + continue } + when (child.tagName) { + "groupId" -> groupId = child.textContent.trim() + "artifactId" -> artifactId = child.textContent.trim() + "version" -> version = child.textContent.trim() + "type" -> type = Type.valueOf(child.textContent.trim().toUpperCase()) + "scope" -> scope = child.textContent.trim() + } + } + if (groupId == null) { + throw GradleException("'' missing in pom") + } + if (artifactId == null) { + throw GradleException("'' missing in pom") + } + if (version == null) { + throw GradleException("'' missing in pom") + } + if (scope == null) { + throw GradleException("'' missing in pom") + } + + return Artifact(groupId, artifactId, version, type, scope) + } + + private fun parseLicense(nodes: NodeList): License? { + if (nodes.length == 0) { + return null + } + val license = nodes.item(0) as Element + val urlElements = license.getElementsByTagName("url") + val url = if (urlElements.length == 0) "" else urlElements.item(0).textContent.trim() + + val nameElements = license.getElementsByTagName("name") + val name = if (nameElements.length == 0) "" else nameElements.item(0).textContent.trim() + return License(name = name, url = url) } + } } diff --git a/ci/fireci/fireci/internal.py b/ci/fireci/fireci/internal.py index c76123e3228..0950d770fc2 100644 --- a/ci/fireci/fireci/internal.py +++ b/ci/fireci/fireci/internal.py @@ -13,7 +13,6 @@ # limitations under the License. import click -import contextlib import functools import glob import itertools @@ -21,6 +20,7 @@ import os import shutil +from contextlib import contextmanager, nullcontext _logger = logging.getLogger('fireci') @@ -30,7 +30,7 @@ def _ensure_dir(directory): os.makedirs(directory) -@contextlib.contextmanager +@contextmanager def _artifact_handler(target_directory, artifact_patterns): _logger.debug( 'Artifacts will be searched for in directories matching {} patterns and placed in {}' @@ -45,7 +45,7 @@ def _artifact_handler(target_directory, artifact_patterns): target_name = os.path.join(target_directory, "_".join(path.split('/'))) _logger.debug('Copying artifact {} to {}'.format(path, target_name)) if os.path.isdir(path): - shutil.copytree(path, target_name) + shutil.copytree(path, target_name, dirs_exist_ok=True) else: shutil.copyfile(path, target_name) @@ -68,8 +68,8 @@ class _CommonOptions: '--artifact-patterns', default=('**/build/test-results', '**/build/reports'), help= - 'Shell-style artifact patterns that are copied into `artifact-target-dir`.'\ - 'Can be specified multiple times.', + 'Shell-style artifact patterns that are copied into `artifact-target-dir`. ' + 'Can be specified multiple times.', multiple=True, type=str, ) @@ -83,30 +83,34 @@ def main(options, **kwargs): setattr(options, k, v) -def ci_command(name=None): +def ci_command(name=None, cls=click.Command, group=main): """Decorator to use for CI commands. The differences from the standard @click.command are: * Allows configuration of artifacts that are uploaded for later viewing in CI. - * Registers the command automatically + * Registers the command automatically. - :param name: Optional name of the task. Defaults to the function name that is decorated with - this decorator. + :param name: Optional name of the task. Defaults to the function name that is decorated with this decorator. + :param cls: Specifies whether the func is a command or a command group. Defaults to `click.Command`. + :param group: Specifies the group the command belongs to. Defaults to the `main` command group. """ def ci_command(f): actual_name = f.__name__ if name is None else name - @main.command(name=actual_name, help=f.__doc__) + @click.command(name=actual_name, cls=cls, help=f.__doc__) @_pass_options @click.pass_context def new_func(ctx, options, *args, **kwargs): with _artifact_handler( options.artifact_target_dir, - options.artifact_patterns): + options.artifact_patterns, + ) if cls is click.Command else nullcontext(): return ctx.invoke(f, *args, **kwargs) + group.add_command(new_func) + return functools.update_wrapper(new_func, f) return ci_command diff --git a/ci/fireci/fireci/plugins.py b/ci/fireci/fireci/plugins.py index 66aebd30f8c..715c8de0884 100644 --- a/ci/fireci/fireci/plugins.py +++ b/ci/fireci/fireci/plugins.py @@ -27,7 +27,7 @@ def discover(): Note: plugins *must* define the `firebaseplugins` package as a namespace package. See: https://packaging.python.org/guides/packaging-namespace-packages/ """ - modules = pkgutil.iter_modules(fireciplugins.__path__, - fireciplugins.__name__ + ".") + modules = pkgutil.walk_packages(fireciplugins.__path__, + fireciplugins.__name__ + ".") for _, name, _ in modules: importlib.import_module(name) diff --git a/ci/fireci/fireci/uploader.py b/ci/fireci/fireci/uploader.py index 1f68799ec28..cb8e617bf2b 100644 --- a/ci/fireci/fireci/uploader.py +++ b/ci/fireci/fireci/uploader.py @@ -17,13 +17,12 @@ import os import requests import subprocess -import urllib.parse _logger = logging.getLogger('fireci.uploader') -def post_report(test_report, metrics_service_url, access_token, metric_type): +def post_report(test_report, metrics_service_url, access_token, metric_type, asynchronous=False): """Post a report to the metrics service backend.""" endpoint = '' @@ -32,6 +31,9 @@ def post_report(test_report, metrics_service_url, access_token, metric_type): elif os.getenv('PROW_JOB_ID'): endpoint = _construct_request_endpoint_for_prow(metric_type) + if asynchronous: + endpoint += '&async=true' + headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'} data = json.dumps(test_report) @@ -62,6 +64,7 @@ def _construct_request_endpoint_for_github_actions(metric_type): return endpoint + def _construct_request_endpoint_for_prow(metric_type): repo_owner = os.getenv('REPO_OWNER') repo_name = os.getenv('REPO_NAME') diff --git a/ci/fireci/fireciplugins/macrobenchmark.py b/ci/fireci/fireciplugins/macrobenchmark.py deleted file mode 100644 index a0fc2f81a39..00000000000 --- a/ci/fireci/fireciplugins/macrobenchmark.py +++ /dev/null @@ -1,319 +0,0 @@ -# Copyright 2021 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. - -import asyncio -import glob -import json -import logging -import os -import random -import re -import shutil -import sys -import tempfile -import uuid - -import click -import numpy -import pystache -import yaml -from google.cloud import storage - -from fireci import ci_command -from fireci import ci_utils -from fireci import uploader -from fireci.dir_utils import chdir - -_logger = logging.getLogger('fireci.macrobenchmark') - - -@click.option( - '--build-only/--no-build-only', - default=False, - help='Whether to only build tracing test apps or to also run them on FTL afterwards' -) -@ci_command() -def macrobenchmark(build_only): - """Measures app startup times for Firebase SDKs.""" - asyncio.run(_launch_macrobenchmark_test(build_only)) - - -async def _launch_macrobenchmark_test(build_only): - _logger.info('Starting macrobenchmark test...') - - artifact_versions = await _assemble_all_artifacts() - _logger.info(f'Artifact versions: {artifact_versions}') - - test_dir = await _prepare_test_directory() - _logger.info(f'Directory for test apps: {test_dir}') - - config = await _process_config_yaml() - _logger.info(f'Processed yaml configurations: {config}') - - tests = [MacrobenchmarkTest(app, artifact_versions, os.getcwd(), test_dir) for app in config['test-apps']] - - _logger.info(f'Building {len(tests)} macrobenchmark test apps...') - # TODO(yifany): investigate why it is much slower with asyncio.gather - # - on corp workstations (9 min) than M1 macbook pro (3 min) - # - with gradle 7.5.1 (9 min) than gradle 6.9.2 (5 min) - # await asyncio.gather(*[x.build() for x in tests]) - for test in tests: - await test.build() - - if not build_only: - _logger.info(f'Submitting {len(tests)} tests to Firebase Test Lab...') - results = await asyncio.gather(*[x.test() for x in tests], return_exceptions=True) - await _post_processing(results) - - _logger.info('Macrobenchmark test finished.') - - -async def _assemble_all_artifacts(): - await (await asyncio.create_subprocess_exec('./gradlew', 'assembleAllForSmokeTests')).wait() - - with open('build/m2repository/changed-artifacts.json') as json_file: - artifacts = json.load(json_file) - return dict(_artifact_key_version(x) for x in artifacts['headGit']) - - -def _artifact_key_version(artifact): - group_id, artifact_id, version = artifact.split(':') - return f'{group_id}:{artifact_id}', version - - -async def _process_config_yaml(): - with open('health-metrics/benchmark/config.yaml') as yaml_file: - config = yaml.safe_load(yaml_file) - for app in config['test-apps']: - app['plugins'] = app.get('plugins', []) - app['traces'] = app.get('traces', []) - app['plugins'].extend(config['common-plugins']) - app['traces'].extend(config['common-traces']) - return config - - -async def _prepare_test_directory(): - test_dir = tempfile.mkdtemp(prefix='benchmark-test-') - - # Required for creating gradle wrapper, as the dir is not defined in the root settings.gradle - open(os.path.join(test_dir, 'settings.gradle'), 'w').close() - - command = ['./gradlew', 'wrapper', '--gradle-version', '7.5.1', '--project-dir', test_dir] - await (await asyncio.create_subprocess_exec(*command)).wait() - - return test_dir - - -async def _post_processing(results): - _logger.info(f'Macrobenchmark results: {results}') - - if os.getenv('CI') is None: - _logger.info('Running locally. Results upload skipped.') - return - - # Upload successful measurements to the metric service - measurements = [] - for result in results: - if not isinstance(result, Exception): - measurements.extend(result) - - log = ci_utils.ci_log_link() - test_report = {'benchmarks': measurements, 'log': log} - - metrics_service_url = 'https://api.firebase-sdk-health-metrics.com' - access_token = ci_utils.gcloud_identity_token() - uploader.post_report(test_report, metrics_service_url, access_token, 'macrobenchmark') - - # Raise exceptions for failed measurements - if any(map(lambda x: isinstance(x, Exception), results)): - _logger.error(f'Exceptions: {[x for x in results if isinstance(x, Exception)]}') - raise click.ClickException('Macrobenchmark test failed with above errors.') - - -class MacrobenchmarkTest: - """Builds the test based on configurations and runs the test on FTL.""" - def __init__( - self, - test_app_config, - artifact_versions, - repo_root_dir, - test_dir, - logger=_logger - ): - self.test_app_config = test_app_config - self.artifact_versions = artifact_versions - self.repo_root_dir = repo_root_dir - self.test_dir = test_dir - self.logger = MacrobenchmarkLoggerAdapter(logger, test_app_config['sdk']) - self.test_app_dir = os.path.join(test_dir, test_app_config['name']) - self.test_results_bucket = 'fireescape-benchmark-results' - self.test_results_dir = str(uuid.uuid4()) - self.gcs_client = storage.Client() - - async def build(self): - """Creates test app project and assembles app and test apks.""" - await self._create_benchmark_projects() - await self._assemble_benchmark_apks() - - async def test(self): - """Runs benchmark tests on FTL and fetches FTL results from GCS.""" - await self._execute_benchmark_tests() - return await self._aggregate_benchmark_results() - - async def _create_benchmark_projects(self): - app_name = self.test_app_config['name'] - self.logger.info(f'Creating test app "{app_name}"...') - - self.logger.info(f'Copying project template files into "{self.test_app_dir}"...') - template_dir = os.path.join(self.repo_root_dir, 'health-metrics/benchmark/template') - shutil.copytree(template_dir, self.test_app_dir) - - self.logger.info(f'Copying gradle wrapper binary into "{self.test_app_dir}"...') - shutil.copy(os.path.join(self.test_dir, 'gradlew'), self.test_app_dir) - shutil.copy(os.path.join(self.test_dir, 'gradlew.bat'), self.test_app_dir) - shutil.copytree(os.path.join(self.test_dir, 'gradle'), os.path.join(self.test_app_dir, 'gradle')) - - with chdir(self.test_app_dir): - mustache_context = await self._prepare_mustache_context() - renderer = pystache.Renderer() - mustaches = glob.glob('**/*.mustache', recursive=True) - for mustache in mustaches: - self.logger.info(f'Processing template file: {mustache}') - result = renderer.render_path(mustache, mustache_context) - original_name = mustache.removesuffix('.mustache') - with open(original_name, 'w') as file: - file.write(result) - - async def _assemble_benchmark_apks(self): - with chdir(self.test_app_dir): - await self._exec_subprocess('./gradlew', ['assemble']) - - async def _execute_benchmark_tests(self): - app_apk_path = glob.glob(f'{self.test_app_dir}/**/app-benchmark.apk', recursive=True)[0] - test_apk_path = glob.glob(f'{self.test_app_dir}/**/macrobenchmark-benchmark.apk', recursive=True)[0] - - self.logger.info(f'App apk: {app_apk_path}') - self.logger.info(f'Test apk: {test_apk_path}') - - ftl_environment_variables = [ - 'clearPackageData=true', - 'additionalTestOutputDir=/sdcard/Download', - 'no-isolated-storage=true', - ] - executable = 'gcloud' - args = ['firebase', 'test', 'android', 'run'] - args += ['--type', 'instrumentation'] - args += ['--app', app_apk_path] - args += ['--test', test_apk_path] - args += ['--device', 'model=oriole,version=32,locale=en,orientation=portrait'] - args += ['--directories-to-pull', '/sdcard/Download'] - args += ['--results-bucket', f'gs://{self.test_results_bucket}'] - args += ['--results-dir', self.test_results_dir] - args += ['--environment-variables', ','.join(ftl_environment_variables)] - args += ['--timeout', '30m'] - args += ['--project', 'fireescape-c4819'] - - await self._exec_subprocess(executable, args) - - async def _prepare_mustache_context(self): - mustache_context = { - 'm2repository': os.path.join(self.repo_root_dir, 'build/m2repository'), - 'plugins': self.test_app_config.get('plugins', []), - 'traces': self.test_app_config.get('traces', []), - 'dependencies': [], - } - - if 'dependencies' in self.test_app_config: - for dep in self.test_app_config['dependencies']: - if '@' in dep: - key, version = dep.split('@', 1) - dependency = {'key': key, 'version': version} - else: - dependency = {'key': dep, 'version': self.artifact_versions[dep]} - mustache_context['dependencies'].append(dependency) - - return mustache_context - - async def _aggregate_benchmark_results(self): - results = [] - blobs = self.gcs_client.list_blobs(self.test_results_bucket, prefix=self.test_results_dir) - files = [x for x in blobs if re.search(r'sdcard/Download/[^/]*\.json', x.name)] - for file in files: - device = re.search(r'([^/]*)/artifacts/', file.name).group(1) - benchmarks = json.loads(file.download_as_bytes())['benchmarks'] - for benchmark in benchmarks: - method = benchmark['name'] - clazz = benchmark['className'].split('.')[-1] - runs = benchmark['metrics']['timeToInitialDisplayMs']['runs'] - results.append({ - 'sdk': self.test_app_config['sdk'], - 'device': device, - 'name': f'{clazz}.{method}', - 'min': min(runs), - 'max': max(runs), - 'p50': numpy.percentile(runs, 50), - 'p90': numpy.percentile(runs, 90), - 'p99': numpy.percentile(runs, 99), - 'unit': 'ms', - }) - self.logger.info(f'Benchmark results: {results}') - return results - - async def _exec_subprocess(self, executable, args): - command = " ".join([executable, *args]) - self.logger.info(f'Executing command: "{command}"...') - - proc = await asyncio.subprocess.create_subprocess_exec( - executable, - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await asyncio.gather( - self._stream_output(executable, proc.stdout), - self._stream_output(executable, proc.stderr) - ) - - await proc.communicate() - if proc.returncode == 0: - self.logger.info(f'"{command}" finished.') - else: - message = f'"{command}" exited with return code {proc.returncode}.' - self.logger.error(message) - raise click.ClickException(message) - - async def _stream_output(self, executable, stream: asyncio.StreamReader): - async for line in stream: - self.logger.info(f'[{executable}] {line.decode("utf-8").strip()}') - - -class MacrobenchmarkLoggerAdapter(logging.LoggerAdapter): - """Decorates log messages for a sdk to make them more distinguishable.""" - - reset_code = '\x1b[m' - - @staticmethod - def random_color_code(): - code = random.randint(16, 231) # https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - return f'\x1b[38;5;{code}m' - - def __init__(self, logger, sdk_name, color_code=None): - super().__init__(logger, {}) - self.sdk_name = sdk_name - self.color_code = self.random_color_code() if color_code is None else color_code - - def process(self, msg, kwargs): - colored = f'{self.color_code}[{self.sdk_name}]{self.reset_code} {msg}' - uncolored = f'[{self.sdk_name}] {msg}' - return colored if sys.stderr.isatty() else uncolored, kwargs diff --git a/ci/fireci/fireciplugins/macrobenchmark/__init__.py b/ci/fireci/fireciplugins/macrobenchmark/__init__.py new file mode 100644 index 00000000000..6d6d1266c32 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 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. diff --git a/ci/fireci/fireciplugins/macrobenchmark/analyze/__init__.py b/ci/fireci/fireciplugins/macrobenchmark/analyze/__init__.py new file mode 100644 index 00000000000..6d6d1266c32 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/analyze/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 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. diff --git a/ci/fireci/fireciplugins/macrobenchmark/analyze/aggregator.py b/ci/fireci/fireciplugins/macrobenchmark/analyze/aggregator.py new file mode 100644 index 00000000000..8c52e90ca51 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/analyze/aggregator.py @@ -0,0 +1,81 @@ +# Copyright 2022 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. + + +import logging +import pandas as pd +import seaborn as sns + +from pathlib import Path + +logger = logging.getLogger('fireci.macrobenchmark') +sns.set() + + +def calculate_statistic(trace: str, device: str, data: pd.DataFrame, output_dir: Path = None): + logger.info(f'Calculating statistics for trace "{trace}" on device "{device}" ...') + + # Calculate percentiles per each run_id + quantiles = [0.1, 0.25, 0.5, 0.75, 0.9] + percentiles = data.groupby('run_id').quantile(quantiles, numeric_only=True) + percentiles.index.set_names('percentile', level=1, inplace=True) + percentiles = percentiles.reset_index(['run_id', 'percentile']) + percentiles = percentiles.pivot(index='run_id', columns='percentile', values='duration') + + def mapper(quantile: float) -> str: return f'p{int(quantile * 100)}' + + percentiles.rename(mapper=mapper, axis='columns', inplace=True) + + # Calculate dispersions of each percentile over all runs + mean = percentiles.mean() + std = percentiles.std() # standard deviation + cv = std / mean # coefficient of variation (relative standard deviation) + mad = (percentiles - percentiles.mean()).abs().mean() # mean absolute deviation + rmad = mad / mean # relative mean absolute deviation (mad / mean) + dispersions = pd.DataFrame([pd.Series(cv, name='cv'), pd.Series(rmad, name='rmad')]) + + # Optionally save percentiles and dispersions to file + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + percentiles.to_json(output_dir.joinpath('percentiles.json'), orient='index') + dispersions.to_json(output_dir.joinpath('dispersions.json'), orient='index') + logger.info(f'Percentiles and dispersions saved in: {output_dir}') + + return percentiles, dispersions + + +def calculate_statistic_diff( + trace: str, + device: str, + control: pd.DataFrame, + experimental: pd.DataFrame, + output_dir: Path = None, +): + logger.info(f'Calculating statistic diff for trace "{trace}" on device "{device}" ...') + + ctl_percentiles, _ = calculate_statistic(trace, device, control, output_dir.joinpath("ctl")) + exp_percentiles, _ = calculate_statistic(trace, device, experimental, output_dir.joinpath("exp")) + + ctl_mean = ctl_percentiles.mean() + exp_mean = exp_percentiles.mean() + + delta = exp_mean - ctl_mean + percentage = delta / ctl_mean + + # Optionally save statistics to file + if output_dir: + output_dir.mkdir(parents=True, exist_ok=True) + delta.to_json(output_dir.joinpath('delta.json')) + percentage.to_json(output_dir.joinpath('percentage.json')) + logger.info(f'Percentiles diff saved in: {output_dir}') diff --git a/ci/fireci/fireciplugins/macrobenchmark/analyze/analyzer.py b/ci/fireci/fireciplugins/macrobenchmark/analyze/analyzer.py new file mode 100644 index 00000000000..daf63b93996 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/analyze/analyzer.py @@ -0,0 +1,102 @@ +# Copyright 2022 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. + +import logging +import tempfile +import pandas as pd + +from .aggregator import calculate_statistic, calculate_statistic_diff +from .plotter import plot_graph, plot_diff_graph +from .utils import collect_data_points, DataPoint +from click import progressbar +from pathlib import Path +from typing import List + + +logger = logging.getLogger('fireci.macrobenchmark') + + +def start( + diff_mode: bool, + ftl_results_dir: List[str], + local_reports_dir: Path, + ctl_ftl_results_dir: List[str], + ctl_local_reports_dir: Path, + exp_ftl_results_dir: List[str], + exp_local_reports_dir: Path, + output_dir: Path +): + logger.info('Starting to analyze macrobenchmark test results ...') + + if not output_dir: + output_dir = Path(tempfile.mkdtemp(prefix='macrobenchmark-analysis-')) + logger.info(f'Created temporary dir "{output_dir}" to save analysis results') + + if not diff_mode: + data_points = collect_data_points(ftl_results_dir, local_reports_dir) + _process(data_points, output_dir) + else: + logger.info('Running in diff mode ...') + ctl_data_points = collect_data_points(ctl_ftl_results_dir, ctl_local_reports_dir) + exp_data_points = collect_data_points(exp_ftl_results_dir, exp_local_reports_dir) + _diff(ctl_data_points, exp_data_points, output_dir) + + logger.info(f'Completed analysis and saved output in: {output_dir}') + + +def _process(data_points: List[DataPoint], output_dir: Path) -> None: + data = pd.DataFrame(data_points) + traces = sorted(data['trace'].unique()) + devices = sorted(data['device'].unique()) + + trace_device_combinations = [(trace, device) for trace in traces for device in devices] + + with progressbar(trace_device_combinations) as combinations: + for trace, device in combinations: + combination_dir = output_dir.joinpath(trace, device) + subset = _filter_subset(data, trace, device) + calculate_statistic(trace, device, subset, combination_dir) + plot_graph(trace, device, subset, combination_dir) + + +def _diff( + ctl_data_points: List[DataPoint], + exp_data_points: List[DataPoint], + output_dir: Path +) -> None: + ctl_data = pd.DataFrame(ctl_data_points) + exp_data = pd.DataFrame(exp_data_points) + all_data = pd.concat([ctl_data, exp_data]) + + traces = sorted(all_data['trace'].unique()) + devices = sorted(all_data['device'].unique()) + + trace_device_combinations = [(trace, device) for trace in traces for device in devices] + + with progressbar(trace_device_combinations) as combinations: + for trace, device in combinations: + combination_dir = output_dir.joinpath(trace, device) + + ctl_subset = _filter_subset(ctl_data, trace, device) + exp_subset = _filter_subset(exp_data, trace, device) + + calculate_statistic_diff(trace, device, ctl_subset, exp_subset, combination_dir) + plot_diff_graph(trace, device, ctl_subset, exp_subset, combination_dir) + + +def _filter_subset(data: pd.DataFrame, trace: str, device: str) -> pd.DataFrame: + return data.loc[ + (data['trace'] == trace) & (data['device'] == device), + ['duration', 'run_id'] + ] diff --git a/ci/fireci/fireciplugins/macrobenchmark/analyze/plotter.py b/ci/fireci/fireciplugins/macrobenchmark/analyze/plotter.py new file mode 100644 index 00000000000..31c98c229c0 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/analyze/plotter.py @@ -0,0 +1,78 @@ +# Copyright 2022 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. + +import logging +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import seaborn as sns + +from pathlib import Path + + +logger = logging.getLogger('fireci.macrobenchmark') +sns.set() + + +def plot_graph(trace: str, device: str, data: pd.DataFrame, output_dir: Path): + logger.info(f'Plotting graphs for trace "{trace}" on device "{device}" ...') + + output_dir.mkdir(parents=True, exist_ok=True) + + unique_run_ids = len(data['run_id'].unique()) + col_wrap = int(np.ceil(np.sqrt(unique_run_ids))) + + histograms = sns.displot(data=data, x='duration', kde=True, col="run_id", col_wrap=col_wrap) + histograms.set_axis_labels(x_var=f'{trace} (ms)') + histograms.set_titles(f'{device} ({{col_var}} = {{col_name}})') + histograms.savefig(output_dir.joinpath('histograms.svg')) + plt.close(histograms.fig) + + distributions = sns.displot( + data=data, x='duration', kde=True, height=8, + hue='run_id', palette='muted', multiple='dodge' + ) + distributions.set_axis_labels(x_var=f'{trace} (ms)').set(title=device) + distributions.savefig(output_dir.joinpath('distributions.svg')) + plt.close(distributions.fig) + + logger.info(f'Graphs saved in: {output_dir}') + + +def plot_diff_graph( + trace: str, + device: str, + control: pd.DataFrame, + experimental: pd.DataFrame, + output_dir: Path +): + logger.info(f'Plotting distribution diff graph for trace "{trace}" on device "{device}" ...') + + output_dir.mkdir(parents=True, exist_ok=True) + + control_run_ids = control['run_id'] + experimental_run_ids = experimental['run_id'] + all_data = pd.concat([control, experimental]) + + palette = {**{x: 'b' for x in control_run_ids}, **{x: 'r' for x in experimental_run_ids}} + + distribution_diff = sns.displot( + data=all_data, x='duration', kde=True, height=8, + hue='run_id', palette=palette, multiple='dodge' + ) + distribution_diff.set_axis_labels(x_var=f'{trace} (ms)').set(title=device) + distribution_diff.savefig(output_dir.joinpath('distribution_diff.svg')) + plt.close(distribution_diff.fig) + + logger.info(f'Graph saved in: {output_dir}') diff --git a/ci/fireci/fireciplugins/macrobenchmark/analyze/utils.py b/ci/fireci/fireciplugins/macrobenchmark/analyze/utils.py new file mode 100644 index 00000000000..131bb909a83 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/analyze/utils.py @@ -0,0 +1,82 @@ +# Copyright 2022 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. + +import json +import logging +import re +import tempfile + +from click import ClickException +from google.cloud import storage +from pathlib import Path +from typing import List, TypedDict + + +logger = logging.getLogger('fireci.macrobenchmark') +DataPoint = TypedDict('DataPoint', {'duration': float, 'device': str, 'trace': str, 'run_id': str}) + + +def collect_data_points(ftl_results_dir: List[str], local_reports_dir: Path) -> List[DataPoint]: + if not ftl_results_dir and not local_reports_dir: + raise ClickException('Neither ftl-results-dir or local-reports-dir is provided.') + elif ftl_results_dir and not local_reports_dir: + temp_dir = _download(ftl_results_dir) + return _extract_raw_data(temp_dir) + elif not ftl_results_dir and local_reports_dir: + return _extract_raw_data(local_reports_dir) + else: + raise ClickException('Should specify either ftl-results-dir or local-reports-dir, not both.') + + +def _download(ftl_results_dirs: List[str]) -> Path: + ftl_results_bucket = 'fireescape-benchmark-results' + gcs = storage.Client() + + temp_dir = tempfile.mkdtemp(prefix='ftl-results-') + for ftl_results_dir in ftl_results_dirs: + blobs = gcs.list_blobs(ftl_results_bucket, prefix=ftl_results_dir) + files = [f for f in blobs if f.name.endswith('.json')] + for file in files: + device = re.search(r'([^/]*)/artifacts/', file.name).group(1) + report_dir = Path(temp_dir).joinpath(ftl_results_dir, device) + report_dir.mkdir(parents=True, exist_ok=True) + filename = file.name.split('/')[-1] + file.download_to_filename(report_dir.joinpath(filename)) + logger.info(f'Downloaded "{file.name}" to "{report_dir}"') + + return Path(temp_dir) + + +def _extract_raw_data(test_reports_dir: Path) -> List[DataPoint]: + data_points: List[DataPoint] = [] + reports = sorted(list(test_reports_dir.rglob("*-benchmarkData.json"))) + for report in reports: + logger.info(f'Processing "{report}" ...') + + run_id = str(report.relative_to(test_reports_dir)).split('/')[0] + with open(report) as file: + obj = json.load(file) + build_context = obj['context']['build'] + device = f'{build_context["device"]}-{build_context["version"]["sdk"]}' + for metric in obj['benchmarks'][0]['metrics'].keys(): + measurements = obj['benchmarks'][0]['metrics'][metric]['runs'] + trace = metric[:-2] # TODO(yifany): .removesuffix('Ms') w/ python 3.9+ + data_points.extend([{ + 'duration': measurement, + 'device': device, + 'trace': trace, + 'run_id': run_id + } for measurement in measurements]) + logger.info(f'Extracted {len(data_points)} data points from reports in "{test_reports_dir}"') + return data_points diff --git a/ci/fireci/fireciplugins/macrobenchmark/commands.py b/ci/fireci/fireciplugins/macrobenchmark/commands.py new file mode 100644 index 00000000000..3c340ece3ea --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/commands.py @@ -0,0 +1,197 @@ +# Copyright 2022 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. + +import asyncio +import click +import json +import logging + +from .analyze import analyzer +from .run import runner +from fireci import ci_command, ci_utils, uploader +from pathlib import Path +from typing import List + +logger = logging.getLogger('fireci.macrobenchmark') + + +@ci_command(cls=click.Group) +def macrobenchmark(): + """Macrobenchmark testing command group.""" + pass + + +@click.option( + '--build-only', + is_flag=True, + default=False, + show_default=True, + help='Build the test projects without running the test.' +) +@click.option( + '--local/--remote', + default=True, + help='Run the test on local devices or Firebase Test Lab.' +) +@click.option( + '--repeat', + default=1, + show_default=True, + help='Number of times to repeat the test (for obtaining more data points).' +) +@click.option( + '--output', + type=click.Path(dir_okay=True, resolve_path=True, path_type=Path), + default='macrobenchmark-output.json', + show_default=True, + help='The file for saving macrobenchmark test output if running on Firebase Test Lab.' +) +@ci_command(group=macrobenchmark) +def run(build_only: bool, local: bool, repeat: int, output: Path): + """Run macrobenchmark test.""" + asyncio.run(runner.start(build_only, local, repeat, output)) + + +@click.option( + '--diff-mode', + is_flag=True, + default=False, + help='Compare two sets of macrobenchmark result.' +) +@click.option( + '--ftl-results-dir', + multiple=True, + help='Firebase Test Lab results directory name. Can be specified multiple times.' +) +@click.option( + '--local-reports-dir', + type=click.Path(dir_okay=True, resolve_path=True, path_type=Path), + help='Path to the directory of local test reports.' +) +@click.option( + '--ctl-ftl-results-dir', + multiple=True, + help='FTL results dir of the control group, if running in diff mode. ' + 'Can be specified multiple times.' +) +@click.option( + '--ctl-local-reports-dir', + type=click.Path(dir_okay=True, resolve_path=True, path_type=Path), + help='Path to the local test reports of the control group, if running in diff mode.' +) +@click.option( + '--exp-ftl-results-dir', + multiple=True, + help='FTL results dir of the experimental group, if running in diff mode. ' + 'Can be specified multiple times.' +) +@click.option( + '--exp-local-reports-dir', + type=click.Path(dir_okay=True, resolve_path=True, path_type=Path), + help='Path to the local test reports of the experimental group, if running in diff mode.' +) +@click.option( + '--output-dir', + type=click.Path(dir_okay=True, resolve_path=True, path_type=Path), + help='The directory for saving macrobenchmark analysis result.' +) +@ci_command(group=macrobenchmark) +def analyze( + diff_mode: bool, + ftl_results_dir: List[str], + local_reports_dir: Path, + ctl_ftl_results_dir: List[str], + ctl_local_reports_dir: Path, + exp_ftl_results_dir: List[str], + exp_local_reports_dir: Path, + output_dir: Path +): + """Analyze macrobenchmark result.""" + analyzer.start( + diff_mode, + ftl_results_dir, + local_reports_dir, + ctl_ftl_results_dir, + ctl_local_reports_dir, + exp_ftl_results_dir, + exp_local_reports_dir, + output_dir, + ) + + +@click.option( + '--pull-request/--push', + required=True, + help='Whether the test is running for a pull request or a push event.' +) +@click.option( + '--changed-modules-file', + type=click.Path(resolve_path=True, path_type=Path), + help='Contains a list of changed modules in the current pull request.' +) +@click.option( + '--repeat', + default=10, + show_default=True, + help='Number of times to repeat the test (for obtaining more data points).' +) +@ci_command(group=macrobenchmark) +def ci(pull_request: bool, changed_modules_file: Path, repeat: int): + """Run tests in CI and upload results to the metric service.""" + + output_path = Path("macrobenchmark-test-output.json") + exception = None + + try: + if pull_request: + asyncio.run( + runner.start( + build_only=False, + local=False, + repeat=repeat, + output=output_path, + changed_modules_file=changed_modules_file, + ) + ) + else: + asyncio.run(runner.start(build_only=False, local=False, repeat=repeat, output=output_path)) + except Exception as e: + logger.error(f"Error: {e}") + exception = e + + with open(output_path) as output_file: + output = json.load(output_file) + project_name = 'test-changed' if pull_request else 'test-all' + ftl_dirs = list(filter(lambda x: x['project'] == project_name, output))[0]['successful_runs'] + ftl_bucket_name = 'fireescape-benchmark-results' + + log = ci_utils.ci_log_link() + ftl_results = list(map(lambda x: {'bucket': ftl_bucket_name, 'dir': x}, ftl_dirs)) + startup_time_data = {'log': log, 'ftlResults': ftl_results} + + if ftl_results: + metric_service_url = 'https://api.firebase-sdk-health-metrics.com' + access_token = ci_utils.gcloud_identity_token() + uploader.post_report( + test_report=startup_time_data, + metrics_service_url=metric_service_url, + access_token=access_token, + metric_type='startup-time', + asynchronous=True + ) + + if exception: + raise exception + +# TODO(yifany): support of command chaining diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/__init__.py b/ci/fireci/fireciplugins/macrobenchmark/run/__init__.py new file mode 100644 index 00000000000..6d6d1266c32 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 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. diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/log_decorator.py b/ci/fireci/fireciplugins/macrobenchmark/run/log_decorator.py new file mode 100644 index 00000000000..177f5a1a3ba --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/log_decorator.py @@ -0,0 +1,51 @@ +# Copyright 2022 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. + +import random +import sys + +from logging import Logger, LoggerAdapter +from typing import Union + + +RESET_CODE = '\x1b[m' + + +class LogDecorator(LoggerAdapter): + """Decorates log messages with colors in console output.""" + + def __init__(self, logger: Union[Logger, LoggerAdapter], key: str): + super().__init__(logger, {}) + self.key = key + self.color_code = self._random_color_code() + + def process(self, msg, kwargs): + colored, uncolored = self._produce_prefix() + result = f'{colored if sys.stderr.isatty() else uncolored} {msg}' + return result, kwargs + + @staticmethod + def _random_color_code(): + code = random.randint(16, 231) # https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + return f'\x1b[38;5;{code}m' + + def _produce_prefix(self): + if hasattr(super(), '_produce_prefix'): + colored_super, uncolored_super = getattr(super(), '_produce_prefix')() + colored = f'{colored_super} {self.color_code}[{self.key}]{RESET_CODE}' + uncolored = f'{uncolored_super} [{self.key}]' + else: + colored = f'{self.color_code}[{self.key}]{RESET_CODE}' + uncolored = f'[{self.key}]' + return colored, uncolored diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/runner.py b/ci/fireci/fireciplugins/macrobenchmark/run/runner.py new file mode 100644 index 00000000000..499663e8f6a --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/runner.py @@ -0,0 +1,148 @@ +# Copyright 2022 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. + +import asyncio +import click +import json +import logging +import tempfile +import yaml + +from .test_project_builder import TestProjectBuilder +from .utils import execute +from pathlib import Path +from typing import Dict, List, Set + + +logger = logging.getLogger('fireci.macrobenchmark') + + +async def start( + build_only: bool, + local: bool, + repeat: int, + output: Path, + changed_modules_file: Path = None +): + logger.info('Starting macrobenchmark test ...') + + config = _process_config_yaml() + product_versions = _assemble_all_products() + test_dir = _prepare_test_directory() + changed_traces = _process_changed_modules(changed_modules_file) + template_project_dir = Path('health-metrics/benchmark/template') + + test_projects = [ + TestProjectBuilder( + test_config, + test_dir, + template_project_dir, + product_versions, + changed_traces, + ).build() for test_config in config['test-apps']] + + if not build_only: + if local: + for test_project in test_projects: + test_project.run_local(repeat) + else: + remote_runs = [test_project.run_remote(repeat) for test_project in test_projects] + results = await asyncio.gather(*remote_runs, return_exceptions=True) + test_outputs = [x for x in results if not isinstance(x, Exception)] + exceptions = [x for x in results if isinstance(x, Exception)] + + with open(output, 'w') as file: + json.dump(test_outputs, file) + logger.info(f'Output of remote testing saved to: {output}') + + if exceptions: + logger.error(f'Exceptions occurred: {exceptions}') + for test_output in test_outputs: + if test_output['exceptions']: + logger.error(f'Exceptions occurred: {test_output["exceptions"]}') + + if exceptions or any(test_output['exceptions'] for test_output in test_outputs): + raise click.ClickException('Macrobenchmark test failed with above exceptions') + + logger.info(f'Completed macrobenchmark test successfully') + + +def _assemble_all_products() -> Dict[str, str]: + execute('./gradlew', 'assembleAllForSmokeTests', logger=logger) + + product_versions: Dict[str, str] = {} + with open('build/m2repository/changed-artifacts.json') as json_file: + artifacts = json.load(json_file) + for artifact in artifacts['headGit']: + group_id, artifact_id, version = artifact.split(':') + product_versions[f'{group_id}:{artifact_id}'] = version + + logger.info(f'Product versions: {product_versions}') + return product_versions + + +def _process_config_yaml(): + with open('health-metrics/benchmark/config.yaml') as yaml_file: + config = yaml.safe_load(yaml_file) + for app in config['test-apps']: + app['plugins'] = app.get('plugins', []) + app['traces'] = app.get('traces', []) + app['plugins'].extend(config['common-plugins']) + app['traces'].extend(config['common-traces']) + return config + + +def _prepare_test_directory() -> Path: + test_dir = tempfile.mkdtemp(prefix='benchmark-test-') + logger.info(f'Temporary test directory created at: {test_dir}') + return Path(test_dir) + + +def _process_changed_modules(path: Path) -> List[str]: + trace_names = { + ":appcheck": ["fire-app-check"], + ":firebase-abt": ["fire-abt"], + ":firebase-appdistribution": ["fire-appdistribution"], + ":firebase-config": ["fire-rc"], + ":firebase-common": ["Firebase", "ComponentDiscovery", "Runtime"], + ":firebase-components": ["Firebase", "ComponentDiscovery", "Runtime"], + ":firebase-database": ["fire-rtdb"], + ":firebase-datatransport": ["fire-transport"], + ":firebase-dynamic-links": ["fire-dl"], + ":firebase-crashlytics": ["fire-cls"], + ":firebase-crashlytics-ndk": ["fire-cls"], + ":firebase-firestore": ["fire-fst"], + ":firebase-functions": ["fire-fn"], + ":firebase-inappmessaging": ["fire-fiam"], + ":firebase-inappmessaging-display": ["fire-fiamd"], + ":firebase-installations": ["fire-installations"], + ":firebase-installations-interop": ["fire-installations"], + ":firebase-messaging": ["fire-fcm"], + ":firebase-messaging-directboot": ["fire-fcm"], + ":firebase-ml-modeldownloader": ["firebase-ml-modeldownloader"], + ":firebase-perf": ["fire-perf"], + ":firebase-storage": ["fire-gcs"], + ":transport": ["fire-transport"], + } + + results: Set[str] = set() + if path: + with open(path) as changed_modules_file: + changed_modules = json.load(changed_modules_file) + for module in changed_modules: + for product in trace_names: + if module.startswith(product): + results.update(trace_names[product]) + logger.info(f"Extracted changed traces {results} from {path}") + return list(results) diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/test_project.py b/ci/fireci/fireciplugins/macrobenchmark/run/test_project.py new file mode 100644 index 00000000000..96f35583e30 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/test_project.py @@ -0,0 +1,108 @@ +# Copyright 2022 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. + +import asyncio +import glob +import re +import shutil + +from .log_decorator import LogDecorator +from .utils import execute, execute_async, generate_test_run_id +from fireci.dir_utils import chdir +from logging import getLogger, Logger, LoggerAdapter +from pathlib import Path +from typing import List, TypedDict, Union + +logger = getLogger('fireci.macrobenchmark') + + +class RemoteTestOutput(TypedDict, total=False): + project: str + successful_runs: List[str] + exceptions: List[str] # Using str due to Exception being not JSON serializable + + +class TestProject: + def __init__(self, name: str, project_dir: Path, custom_logger: Union[Logger, LoggerAdapter]): + self.name = name + self.test_project_dir = project_dir + self.logger = custom_logger + + def run_local(self, repeat: int): + self.logger.info(f'Running test locally for {repeat} times ...') + local_reports_dir = self.test_project_dir.joinpath('_reports') + + with chdir(self.test_project_dir): + for index in range(repeat): + run_id = generate_test_run_id() + run_logger = LogDecorator(self.logger, f'run-{index}') + run_logger.info(f'Run-{index}: {run_id}') + execute('./gradlew', ':macrobenchmark:connectedCheck', logger=run_logger) + + reports = self.test_project_dir.rglob('build/**/*-benchmarkData.json') + run_dir = local_reports_dir.joinpath(run_id) + for report in reports: + device = re.search(r'benchmark/connected/([^/]*)/', str(report)).group(1) + device_dir = run_dir.joinpath(device) + device_dir.mkdir(parents=True, exist_ok=True) + shutil.copy(report, device_dir) + run_logger.debug(f'Copied report file "{report}" to "{device_dir}"') + + self.logger.info(f'Finished all {repeat} runs, local reports dir: "{local_reports_dir}"') + + async def run_remote(self, repeat: int) -> RemoteTestOutput: + self.logger.info(f'Running test remotely for {repeat} times ...') + + with chdir(self.test_project_dir): + await execute_async('./gradlew', 'assemble', logger=self.logger) + app_apk_path = glob.glob('**/app-benchmark.apk', recursive=True)[0] + test_apk_path = glob.glob('**/macrobenchmark-benchmark.apk', recursive=True)[0] + self.logger.info(f'App apk: "{app_apk_path}", Test apk: "{test_apk_path}"') + + async def run(index: int, run_id: str) -> str: + run_logger = LogDecorator(self.logger, f'run-{index}') + run_logger.info(f'Run-{index}: {run_id}') + ftl_environment_variables = [ + 'clearPackageData=true', + 'additionalTestOutputDir=/sdcard/Download', + 'no-isolated-storage=true', + ] + executable = 'gcloud' + args = ['firebase', 'test', 'android', 'run'] + args += ['--type', 'instrumentation'] + args += ['--app', app_apk_path] + args += ['--test', test_apk_path] + args += ['--device', 'model=oriole,version=32,locale=en,orientation=portrait'] + args += ['--device', 'model=redfin,version=30,locale=en,orientation=portrait'] + args += ['--directories-to-pull', '/sdcard/Download'] + args += ['--results-bucket', 'fireescape-benchmark-results'] + args += ['--results-dir', run_id] + args += ['--environment-variables', ','.join(ftl_environment_variables)] + args += ['--timeout', '45m'] + args += ['--project', 'fireescape-c4819'] + await execute_async(executable, *args, logger=run_logger) + return run_id + + runs = [run(i, generate_test_run_id()) for i in range(repeat)] + results = await asyncio.gather(*runs, return_exceptions=True) + successes = [x for x in results if not isinstance(x, Exception)] + exceptions = [x for x in results if isinstance(x, Exception)] + + self.logger.info(f'Finished all {repeat} runs, successes: {successes}, failures: {exceptions}') + + return RemoteTestOutput( + project=self.name, + successful_runs=successes, + exceptions=[str(e) for e in exceptions] + ) diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/test_project_builder.py b/ci/fireci/fireciplugins/macrobenchmark/run/test_project_builder.py new file mode 100644 index 00000000000..c384dcb86f3 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/test_project_builder.py @@ -0,0 +1,96 @@ +# Copyright 2022 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. + +import logging +import os +import pystache +import shutil + +from .log_decorator import LogDecorator +from .test_project import TestProject +from .utils import execute +from pathlib import Path +from typing import Any, Dict, List + + +logger = logging.getLogger('fireci.macrobenchmark') + + +class TestProjectBuilder: + def __init__( + self, + test_config: Any, + test_dir: Path, + template_project_dir: Path, + product_versions: Dict[str, str], + changed_traces: List[str], + ): + self.test_config = test_config + self.template_project_dir = template_project_dir + self.product_versions = product_versions + self.changed_traces = changed_traces + + self.name = 'test-changed' if changed_traces else 'test-all' + self.logger = LogDecorator(logger, self.name) + self.project_dir = test_dir.joinpath(self.name) + + def build(self) -> TestProject: + self.logger.info(f'Creating test project "{self.name}" ...') + + self._copy_template_project() + self._flesh_out_mustache_template_files() + self._download_gradle_wrapper() + + self.logger.info(f'Test project "{self.name}" created at "{self.project_dir}"') + return TestProject(self.name, self.project_dir, self.logger) + + def _copy_template_project(self): + shutil.copytree(self.template_project_dir, self.project_dir) + self.logger.debug(f'Copied project template files into "{self.project_dir}"') + + def _download_gradle_wrapper(self): + args = ['wrapper', '--gradle-version', '7.5.1', '--project-dir', str(self.project_dir)] + execute('./gradlew', *args, logger=self.logger) + self.logger.debug(f'Created gradle wrapper in "{self.project_dir}"') + + def _flesh_out_mustache_template_files(self): + mustache_context = { + 'm2repository': os.path.abspath('build/m2repository'), + 'plugins': self.test_config.get('plugins', []), + 'traces': [], + 'dependencies': [], + } + + if 'dependencies' in self.test_config: + for dep in self.test_config['dependencies']: + if '@' in dep: + key, version = dep.split('@', 1) + dependency = {'key': key, 'version': version} + else: + dependency = {'key': dep, 'version': self.product_versions[dep]} + mustache_context['dependencies'].append(dependency) + + if 'traces' in self.test_config: + for trace in self.test_config['traces']: + if not self.changed_traces or trace in self.changed_traces: + mustache_context['traces'].append(trace) + + renderer = pystache.Renderer() + mustaches = self.project_dir.rglob('**/*.mustache') + for mustache in mustaches: + self.logger.debug(f'Processing template file: {mustache}') + result = renderer.render_path(mustache, mustache_context) + original_name = str(mustache)[:-9] # TODO(yifany): .removesuffix('.mustache') w/ python 3.9+ + with open(original_name, 'w') as file: + file.write(result) diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/utils.py b/ci/fireci/fireciplugins/macrobenchmark/run/utils.py new file mode 100644 index 00000000000..32e90193438 --- /dev/null +++ b/ci/fireci/fireciplugins/macrobenchmark/run/utils.py @@ -0,0 +1,65 @@ +# Copyright 2022 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. + +import datetime +import string +import random + +from asyncio import create_subprocess_exec +from asyncio.subprocess import PIPE as ASYNC_PIPE, STDOUT as ASYNC_STDOUT +from logging import Logger, LoggerAdapter +from subprocess import Popen, PIPE, STDOUT +from typing import Union + + +def generate_test_run_id() -> str: + now = datetime.datetime.now() + date = now.date() + time = now.time() + name = ''.join(random.choices(string.ascii_letters, k=4)) + return f'{date}_{time}_{name}' + + +def execute(program: str, *args: str, logger: Union[Logger, LoggerAdapter]) -> None: + command = " ".join([program, *args]) + logger.info(f'Executing subprocess: "{command}" ...') + + popen = Popen([program, *args], stdout=PIPE, stderr=STDOUT) + for line in popen.stdout: + logger.info(f'[{program}] {line.decode("utf-8").strip()}') + popen.communicate() + + if popen.returncode == 0: + logger.info(f'"{command}" succeeded') + else: + message = f'"{command}" failed with return code {popen.returncode}' + logger.error(message) + raise RuntimeError(message) + + +async def execute_async(program: str, *args: str, logger: Union[Logger, LoggerAdapter]) -> None: + command = " ".join([program, *args]) + logger.info(f'Executing subprocess: "{command}" ...') + + process = await create_subprocess_exec(program, *args, stdout=ASYNC_PIPE, stderr=ASYNC_STDOUT) + async for line in process.stdout: + logger.info(f'[{program}] {line.decode("utf-8").strip()}') + await process.communicate() + + if process.returncode == 0: + logger.info(f'"{command}" succeeded') + else: + message = f'"{command}" failed with return code {process.returncode}' + logger.error(message) + raise RuntimeError(message) diff --git a/ci/fireci/setup.cfg b/ci/fireci/setup.cfg index 4bc55ca8ea5..466898d3cb6 100644 --- a/ci/fireci/setup.cfg +++ b/ci/fireci/setup.cfg @@ -5,12 +5,15 @@ version = 0.1 [options] install_requires = protobuf==3.19 - click==7.0 - google-cloud-storage==1.44.0 + click==8.1.3 + google-cloud-storage==2.5.0 + mypy==0.991 numpy==1.23.1 + pandas==1.5.1 PyGithub==1.55 pystache==0.6.0 requests==2.23.0 + seaborn==0.12.1 PyYAML==6.0.0 [options.extras_require] @@ -20,3 +23,22 @@ test = [options.entry_points] console_scripts = fireci = fireci.main:cli + +[mypy] +strict_optional = False +[mypy-google.cloud] +ignore_missing_imports = True +[mypy-matplotlib] +ignore_missing_imports = True +[mypy-matplotlib.pyplot] +ignore_missing_imports = True +[mypy-pandas] +ignore_missing_imports = True +[mypy-pystache] +ignore_missing_imports = True +[mypy-requests] +ignore_missing_imports = True +[mypy-seaborn] +ignore_missing_imports = True +[mypy-yaml] +ignore_missing_imports = True diff --git a/contributor-docs/README.md b/contributor-docs/README.md new file mode 100644 index 00000000000..c7b37f2b6f3 --- /dev/null +++ b/contributor-docs/README.md @@ -0,0 +1,15 @@ +--- +nav_order: 1 +permalink: / +--- + +# Contributor documentation + +This site is a collection of docs and best practices for contributors to Firebase Android SDKs. +It describes how Firebase works on Android and provides guidance on how to build/maintain a Firebase SDK. + +## New to Firebase? + +- [Development Environment Setup]({{ site.baseurl }}{% link onboarding/env_setup.md %}) +- [Creating a new SDK]({{ site.baseurl }}{% link onboarding/new_sdk.md %}) +- [How Firebase works]({{ site.baseurl }}{% link how_firebase_works.md %}) diff --git a/contributor-docs/_config.yml b/contributor-docs/_config.yml new file mode 100644 index 00000000000..bb995ce78b3 --- /dev/null +++ b/contributor-docs/_config.yml @@ -0,0 +1,85 @@ +title: Contributor documentation +description: Documentation and best practices for Android SDK development +logo: "https://firebase.google.com/downloads/brand-guidelines/SVG/logo-logomark.svg" + +remote_theme: just-the-docs/just-the-docs@v0.4.0.rc3 +plugins: +- jekyll-remote-theme + +color_scheme: light + +# Aux links for the upper right navigation +aux_links: + "SDK Github Repo": + - "//github.com/firebase/firebase-android-sdk" + +# Enable or disable the site search +# Supports true (default) or false +search_enabled: true +search: + # Split pages into sections that can be searched individually + # Supports 1 - 6, default: 2 + heading_level: 6 + # Maximum amount of previews per search result + # Default: 3 + previews: 2 + # Maximum amount of words to display before a matched word in the preview + # Default: 5 + preview_words_before: 3 + # Maximum amount of words to display after a matched word in the preview + # Default: 10 + preview_words_after: 3 + # Set the search token separator + # Default: /[\s\-/]+/ + # Example: enable support for hyphenated search words + tokenizer_separator: /[\s/]+/ + # Display the relative url in search results + # Supports true (default) or false + rel_url: true + # Enable or disable the search button that appears in the bottom right corner of every page + # Supports true or false (default) + button: false + +mermaid: + # Version of mermaid library + # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/ + version: "9.2.2" + # Put any additional configuration, such as setting the theme, in _includes/mermaid_config.js + +# Enable or disable heading anchors +heading_anchors: true +permalink: pretty + +callouts_level: quiet +callouts: + highlight: + color: yellow + important: + title: Important + color: blue + new: + title: New + color: green + note: + title: Note + color: purple + warning: + title: Warning + color: red + +# Back to top link +back_to_top: true +back_to_top_text: "Back to top" + +# Footer last edited timestamp +last_edit_timestamp: true # show or hide edit time - page must have `last_modified_date` defined in the frontmatter +last_edit_time_format: "%b %e %Y at %I:%M %p" # uses ruby's time format: https://ruby-doc.org/stdlib-2.7.0/libdoc/time/rdoc/Time.html + + +# Footer "Edit this page on GitHub" link text +gh_edit_link: true # show or hide edit this page link +gh_edit_link_text: "Edit this page on GitHub" +gh_edit_repository: "https://github.com/firebase/firebase-android-sdk" # the github URL for your repo +gh_edit_branch: "master" # the branch that your docs is served from +gh_edit_source: "contributor-docs" # the source that your files originate from +gh_edit_view_mode: "edit" # "tree" or "edit" if you want the user to jump into the editor immediately \ No newline at end of file diff --git a/contributor-docs/_includes/favicon.html b/contributor-docs/_includes/favicon.html new file mode 100644 index 00000000000..c244949e06e --- /dev/null +++ b/contributor-docs/_includes/favicon.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contributor-docs/_includes/nav_footer_custom.html b/contributor-docs/_includes/nav_footer_custom.html new file mode 100644 index 00000000000..177d34a0fb5 --- /dev/null +++ b/contributor-docs/_includes/nav_footer_custom.html @@ -0,0 +1,3 @@ +
+ © Google LLC {{ 'now' | date: "%Y" }} +
\ No newline at end of file diff --git a/contributor-docs/best_practices/best_practices.md b/contributor-docs/best_practices/best_practices.md new file mode 100644 index 00000000000..1fc465d3ed7 --- /dev/null +++ b/contributor-docs/best_practices/best_practices.md @@ -0,0 +1,7 @@ +--- +has_children: true +permalink: /best_practices/ +nav_order: 5 +--- + +# Best Practices diff --git a/contributor-docs/best_practices/dependency_injection.md b/contributor-docs/best_practices/dependency_injection.md new file mode 100644 index 00000000000..3b5de828998 --- /dev/null +++ b/contributor-docs/best_practices/dependency_injection.md @@ -0,0 +1,247 @@ +--- +parent: Best Practices +--- + +# Dependency Injection + +While [Firebase Components]({{ site.baseurl }}{% link components/components.md %}) provides basic +Dependency Injection capabilities for interop between Firebase SDKs, it's not ideal as a general purpose +DI framework for a few reasons, to name some: + +* It's verbose, i.e. requires manually specifying dependencies and constructing instances of components in Component + definitions. +* It has a runtime cost, i.e. initialization time is linear in the number of Components present in the graph + +As a result using [Firebase Components]({{ site.baseurl }}{% link components/components.md %}) is appropriate only +for inter-SDK injection and scoping instances per `FirebaseApp`. + +On the other hand, manually instantiating SDKs is often tedious, errorprone, and leads to code smells +that make code less testable and couples it to the implementation rather than the interface. For more context see +[Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) and [Motivation](https://github.com/google/guice/wiki/Motivation). + +{: .important } +It's recommended to use [Dagger](https://dagger.dev) for internal dependency injection within the SDKs and +[Components]({{ site.baseurl }}{% link components/components.md %}) to inject inter-sdk dependencies that are available only at +runtime into the [Dagger Graph](https://dagger.dev/dev-guide/#building-the-graph) via +[builder setters](https://dagger.dev/dev-guide/#binding-instances) or [factory arguments](https://dagger.dev/api/latest/dagger/Component.Factory.html). + +See: [Dagger docs](https://dagger.dev) +See: [Dagger tutorial](https://dagger.dev/tutorial/) + +{: .highlight } +While Hilt is the recommended way to use dagger in Android applications, it's not suitable for SDK/library development. + +## How to get started + +Since [Dagger](https://dagger.dev) does not strictly follow semver and requires the dagger-compiler version to match the dagger library version, +it's not safe to depend on it via a pom level dependency, see [This comment](https://github.com/firebase/firebase-android-sdk/issues/1677#issuecomment-645669608) for context. For this reason in Firebase SDKs we "vendor/repackage" Dagger into the SDK itself under +`com.google.firebase.{sdkname}.dagger`. While it incurs in a size increase, it's usually on the order of a couple of KB and is considered +negligible. + +To use Dagger in your SDK use the following in your Gradle build file: + +```groovy +plugins { + id("firebase-vendor") +} + +dependencies { + implementation(libs.javax.inject) + vendor(libs.dagger.dagger) { + exclude group: "javax.inject", module: "javax.inject" + } + annotationProcessor(libs.dagger.compiler) +} +``` + +## General Dagger setup + +As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), all components are scoped per `FirebaseApp` +meaning there is a single instance of the component within a given `FirebaseApp`. + +This makes it a natural fit to get all inter-sdk dependencies and instatiate the Dagger component inside the `ComponentRegistrar`. + +```kotlin +class MyRegistrar : ComponentRegistrar { + override fun getComponents() = listOf( + Component.builder(MySdk::class.java) + .add(Dependency.required(FirebaseOptions::class.java)) + .add(Dependency.optionalProvider(SomeInteropDep::class.java)) + .factory(c -> DaggerMySdkComponent.builder() + .setFirebaseApp(c.get(FirebaseApp::class.java)) + .setSomeInterop(c.getProvider(SomeInteropDep::class.java)) + .build() + .getMySdk()) + .build() +} +``` + +Here's a simple way to define the dagger component: + +```kotlin +@Component(modules = MySdkComponent.MainModule::class) +@Singleton +interface MySdkComponent { + // Informs dagger that this is one of the types we want to be able to create + // In this example we only care about MySdk + fun getMySdk() : MySdk + + // Tells Dagger that some types are not available statically and in order to create the component + // it needs FirebaseApp and Provider + @Component.Builder + interface Builder { + @BindsInstance fun setFirebaseApp(app: FirebaseApp) + @BindsInstance fun setSomeInterop(interop: com.google.firebase.inject.Provider) + fun build() : MySdkComponent + } + + @Module + interface MainModule { + // define module @Provides and @Binds here + } +} +``` + +The only thing left to do is to properly annotate `MySdk`: + +```kotlin +@Singleton +class MySdk @Inject constructor(app: FirebaseApp, interopAdapter: MySdkAdapter) { + fun someMethod() { + interopAdapter.doInterop() + } +} + +@Singleton +class MySdkInteropAdapter @Inject constructor(private val interop: com.google.firebase.inject.Provider) { + fun doInterop() { + interop.get().doStuff() + } +} +``` + +## Scope + +Unlike Component, Dagger does not use singleton scope by default and instead injects a new instance of a type at each injection point, +in the example above we want `MySdk` and `MySdkInteropAdapter` to be singletons so they are are annotated with `@Singleton`. + +See [Scoped bindings](https://dagger.dev/dev-guide/#singletons-and-scoped-bindings) for more details. + +### Support multiple instances of the SDK per `FirebaseApp`(multi-resource) + +As mentioned in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), some SDKs support multi-resource mode, +which effectively means that there are 2 scopes at play: + +1. `@Singleton` scope that the main `MultiResourceComponent` has. +2. Each instance of the sdk will have its own scope. + +```mermaid +flowchart LR + subgraph FirebaseApp + direction TB + subgraph FirebaseComponents + direction BT + subgraph GlobalComponents[Outside of SDK] + direction LR + + FirebaseOptions + SomeInterop + Executor["@Background Executor"] + end + + subgraph DatabaseComponent["@Singleton DatabaseMultiDb"] + direction TB + subgraph Singleton["@Singleton"] + SomeImpl -.-> SomeInterop + SomeImpl -.-> Executor + end + + subgraph Default["@DbScope SDK(default)"] + MainClassDefault[FirebaseDatabase] --> SomeImpl + SomeOtherImplDefault[SomeOtherImpl] -.-> FirebaseOptions + MainClassDefault --> SomeOtherImplDefault + end + subgraph MyDbName["@DbScope SDK(myDbName)"] + MainClassMyDbName[FirebaseDatabase] --> SomeImpl + SomeOtherImplMyDbName[SomeOtherImpl] -.-> FirebaseOptions + MainClassMyDbName --> SomeOtherImplMyDbName + end + end + end + end + + classDef green fill:#4db6ac + classDef blue fill:#1a73e8 + class GlobalComponents green + class DatabaseComponent green + class Default blue + class MyDbName blue +``` + +As you can see above, `DatabaseMultiDb` and `SomeImpl` are singletons, while `FirebaseDatabase` and `SomeOtherImpl` are scoped per `database name`. + +It can be easily achieved with the help of [Dagger's subcomponents](https://dagger.dev/dev-guide/subcomponents). + +For example: + +```kotlin +@Scope +annotation class DbScope + +@Component(modules = DatabaseComponent.MainModule::class) +interface DatabaseComponent { + fun getMultiDb() : DatabaseMultiDb + + @Component.Builder + interface Builder { + // usual setters for Firebase component dependencies + // ... + fun build() : DatabaseComponent + } + + @Module(subcomponents = DbInstanceComponent::class) + interface MainModule {} + + @Subcomponent(modules = DbInstanceComponent.InstanceModule::class) + @DbScope + interface DbInstanceComponent { + fun factory() : Factory + + @Subcomponent.Factory + interface Factory { + fun create(@BindsInstance @Named("dbName") dbName: String) + } + } + + @Module + interface InstanceModule { + // ... + } +} +``` + +Annotating `FirebaseDatabase`: + +```kotlin +@DbScope +class FirebaseDatabase @Inject constructor(options: FirebaseOptions, @Named dbName: String) { + // ... +} +``` + +Implementing `DatabaseMultiDb`: + +```kotlin +@Singleton +class DatabaseMultiDb @Inject constructor(private val factory: DbInstanceComponent.Factory) { + private val instances = mutableMapOf() + + @Synchronized + fun get(dbName: String) : FirebaseDatabase { + if (!instances.containsKey(dbName)) { + mInstances.put(dbName, factory.create(dbName)) + } + return mInstances.get(dbName); + } +} +``` diff --git a/contributor-docs/components/components.md b/contributor-docs/components/components.md new file mode 100644 index 00000000000..de624274aab --- /dev/null +++ b/contributor-docs/components/components.md @@ -0,0 +1,243 @@ +--- +has_children: true +permalink: /components/ +nav_order: 4 +--- + +# Firebase Components +{: .no_toc} + +1. TOC +{:toc} + +Firebase is known for being easy to use and requiring no/minimal configuration at runtime. +Just adding SDKs to the app makes them discover each other to provide additional functionality, +e.g. `Firestore` automatically integrates with `Auth` if present in the app. + +* Firebase SDKs have required and optional dependencies on other Firebase SDKs +* SDKs have different initialization requirements, e.g. `Analytics` and `Crashlytics` must be + initialized upon application startup, while some are initialized on demand only. + +To accommodate these requirements Firebase uses a component model that discovers SDKs present in the app, +determines their dependencies and provides them to dependent SDKs via a `Dependency Injection` mechanism. + +This page describes the aforementioned Component Model, how it works and why it's needed. + +## Design Considerations + +### Transparent/invisible to 3p Developers + +To provide good developer experience, we don't want developers to think about how SDKs work and interoperate internally. +Instead we want our SDKs to have a simple API surface that hides all of the internal details. +Most products have an API surface that allows developers to get aninstance of a given SDK via `FirebaseFoo.getInstance()` +and start using it right away. + +### Simple to use and integrate with for component developers + +* The component model is lightweight in terms of integration effort. It is not opinionated on how components are structured. +* The component model should require as little cooperation from components runtime as possible. +* It provides component developers with an API that is easy to use correctly, and hard to use incorrectly. +* Does not sacrifice testability of individual components in isolation + +### Performant at startup and initialization + +The runtime does as little work as possible during initialization. + +## What is a Component? + +A Firebase Component is an entity that: + +* Implements one or more interfaces +* Has a list of dependencies(required or optional). See [Dependencies]({{ site.baseurl }}{% link components/dependencies.md %}) +* Has initialization requirements(e.g. eager in default app) +* Defines a factory creates an instance of the component’s interface given it's dependencies. + (In other words describes how to create the given component.) + +Example: + +```java +// Defines a component that is registered as both `FirebaseAuth` and `InternalAuthProvider`. +Component auth = Component.builder(FirebaseAuth.class, InternalAuthProvider.class) + // Declares dependencies + .add(Dependency.required(FirebaseOptions.class)) + // Defines a factory + .factory(container -> new FirebaseAuth(container.get(FirebaseOptions.class))) + .eagerInDefaultApp() // alwaysEager() or lazy(), lazy is the default. + .build() +``` + +All components are singletons within a Component Container(e.g. one instance per FirebaseApp). +There are however SDKs that need the ability to expose multiple objects per FirebaseApp, +for example RTBD(as well as Storage and Firestore) has multidb support which allows developers +to access one or more databases within one FirebaseApp. To address this requirement, +SDKs have to register their components in the following form(or similar): + +```java +// This is the singleton holder of different instances of FirebaseDatabase. +interface RtdbComponent { + FirebaseDatabase getDefault(); + FirebaseDatabase get(String databaseName); +} +``` + +As you can see in the previous section, components are just values and don't have any behavior per se, +essentially they are just blueprints of how to create them and what dependencies they need. + +So there needs to be some ComponentRuntime that can discover and wire them together into a dependency graph, +in order to do that, there needs to be an agreed upon location where SDKs can register the components they provide. + +The next 2 sections describe how it's done. + +## Component Registration + +In order to define the `Components` an SDK provides, it needs to define a class that implements `ComponentRegistrar`, +this class contains all component definitions the SDK wants to register with the runtime: + +```java +public class MyRegistrar implements ComponentRegistrar { + /// Returns a one or more Components that will be registered in + /// FirebaseApp and participate in dependency resolution and injection. + @Override + public Collection> getComponents() { + Arrays.asList(Component.builder(MyType.class) + /* ... */ + .build()); + } +} +``` + +## Component Discovery + +In addition to creating the `ComponentRegistrar` class, SDKs also need to add them to their `AndroidManifest.xml` under `ComponentDiscoveryService`: + +```xml + + + + + + + +``` + +When the final app is built, manifest registrar entries will all end up inside the above `service` as metadata key- value pairs. +At this point `FirebaseApp` will instantiate them and use the `ComponentRuntime` to construct the component graph. + +## Dependency resolution and initialization + +### Definitions and constraints + +* **Component A depends on Component B** if `B` depends on an `interface` that `A` implements. +* **For any Interface I, only one component is allowed to implement I**(with the exception of + [Set Dependencies]({{ site.baseurl }}{% link components/dependencies.md %}#set-dependencies)). If this invariant is violated, the container will + fail to start at runtime. +* **There must not be any dependency cycles** among components. See Dependency Cycle Resolution on how this limitation can + be mitigated +* **Components are initialized lazily by default**(unless a component is declared eager) and are initialized when requested + by an application either directly or transitively. + +The initialization phase of the FirebaseApp will consist of the following steps: + +1. Get a list of available FirebaseComponents that were discovered by the Discovery mechanism +2. Topologically sort components based on their declared dependencies - failing if a dependency cycle is detected or multiple implementations are registered for any interface. +3. Store a map of {iface -> ComponentFactory} so that components can be instantiated on demand(Note that component instantiation does not yet happen) +4. Initialize EAGER components or schedule them to initialize on device unlock, if in direct boot mode. + +### Initialization example + +Below is an example illustration of the state of the component graph after initialization: + +```mermaid +flowchart TD + Analytics --> Installations + Auth --> Context + Auth --> FirebaseOptions + Context[android.os.Context] + Crashlytics --> Installations + Crashlytics --> FirebaseApp + Crashlytics --> FirebaseOptions + Crashlytics -.-> Analytics + Crashlytics --> Context + Database -.-> Auth + Database --> Context + Database --> FirebaseApp + Database --> FirebaseOptions + Firestore -.-> Auth + Messaging --> Installations + Messaging --> FirebaseOptions + Messaging --> Context + RemoteConfig --> FirebaseApp + RemoteConfig --> Context + RemoteConfig --> Installations + + + classDef eager fill:#4db66e,stroke:#4db6ac,color:#000; + classDef transitive fill:#4db6ac,stroke:#4db6ac,color:#000; + classDef always fill:#1a73e8,stroke:#7baaf7,color:#fff; + + class Analytics eager + class Crashlytics eager + class Context always + class FirebaseOptions always + class FirebaseApp always + class Installations transitive +``` + +There are **2 explicitly eager** components in this example: `Crashlytics` and `Analytics`. +These components are initialized when `FirebaseApp` is initialized. `Installations` is initialized eagerly because +eager components depends on it(see Prefer Lazy dependencies to avoid this as mush as possible). +`FirebaseApp`, `FirebaseOptions` and `Android Context` are always present in the Component Container and are considered initialized as well. + +*The rest of the components are left uninitialized and will remain so until the client application requests them or an eager +component initializes them by using a Lazy dependency.* +For example, if the application calls `FirebaseDatabase.getInstance()`, the container will initialize `Auth` and `Database` +and will return `Database` to the user. + +### Support multiple instances of the SDK per `FirebaseApp`(multi-resource) + +Some SDKs support multi-resource mode of operation, where it's possible to create more than one instance per `FirebaseApp`. + +Examples: + +* RTDB allows more than one database in a single Firebase project, so it's possible to instantiate one instance of the sdk per datbase + +```kotlin +val rtdbOne = Firebase.database(app) // uses default database +val rtdbTwo = Firebase.database(app, "dbName") +``` + +* Firestore, functions, and others support the same usage pattern + +To allow for that, such SDKs register a singleton "MultiResource" [Firebase component]({{ site.baseurl }}{% link components/components.md %}), +which creates instances per resource(e.g. db name). + +Example + +```kotlin +class DatabaseComponent(private val app: FirebaseApp, private val tokenProvider: InternalTokenProvider) { + private val instances: MutableMap = new HashMap<>(); + + @Synchronized + fun get(dbName: String) : FirebaseDatabase { + if (!instances.containsKey(dbName)) { + instances.put(dbName, FirebaseDatabase(app, tokenProvider, dbName)) + } + return instances.get(dbName); + } +} + +class FirebaseDatabase( + app: FirebaseApp, + tokenProvider: InternalTokenProvider, + private val String dbName) + + companion object { + fun getInstance(app : FirebaseApp) = getInstance("default") + fun getInstance(app : FirebaseApp, dbName: String) = + app.get(DatabaseComponent::class.java).get("default") + } + +``` diff --git a/contributor-docs/components/dependencies.md b/contributor-docs/components/dependencies.md new file mode 100644 index 00000000000..587fe109ab1 --- /dev/null +++ b/contributor-docs/components/dependencies.md @@ -0,0 +1,188 @@ +--- +parent: Firebase Components +--- + +# Dependencies +{: .no_toc} + +1. TOC +{:toc} + +This page gives an overview of the different dependency types supported by the Components Framework. + +## Background + +As discussed in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}), in order +for a `Component` to be injected with the things it needs to function, it has to declare its dependencies. +These dependencies are then made available and injected into `Components` at runtime. + +Firebase Components provide different types of dependencies. + +## Lazy vs Eager dependencies + +When it comes to initialize a component, there are 2 ways of provide its dependencies. + +### Direct Injection + +With this type of injection, the component gets an instance of its dependency directly. + +```kotlin +class MyComponent(private val dep : MyDep) { + fun someMethod() { + dep.use(); + } +} +``` + +As you can see above the component's dependency is passed by value directly, +which means that the dependency needs to be fully initialized before +it's handed off to the requesting component. As a result `MyComponent` may have to pay the cost +of initializing `MyDep` just to be created. + +### Lazy/Provider Injection + +With this type of injection, instead of getting an instance of the dependency directly, the dependency +is passed into the `Component` with the help of a `com.google.firebase.inject.Provider` + +```java +public interface Provider { T get(); } +``` + +```kotlin +class MyComponent(private val dep : Provider) { + fun someMethod() { + // Since all components are singletons, each call to + // get() will return the same instance. + dep.get().use(); + } +} +``` + +On the surface this does not look like a big change, but it has an important side effect. In order to create +an instance of `MyComponent`, we don't need to initialize `MyDep` anymore. Instead, initialization can be +delayed until `MyDep` is actually used. + +It is also benefitial to use a `Provider` in the context of [Play's dynamic feature delivery](https://developer.android.com/guide/playcore/feature-delivery). +See [Dynamic Module Support]({{ site.baseurl }}{% link components/dynamic_modules.md %}) for more details. + +## Required dependencies + +This type of dependency informs the `ComponentRuntime` that a given `Component` cannot function without a dependency. +When the dependency is missing during initialization, `ComponentRuntime` will throw a `MissingDependencyException`. +This type of dependency is useful for built-in components that are always present like `Context`, `FirebaseApp`, +`FirebaseOptions`, [Executors]({{ site.baseurl }}{% link components/executors.md %}). + +To declare a required dependency use one of the following in your `ComponentRegistrar`: + +```java + // Required directly injected dependency + .add(Dependency.required(MyDep.class)) + // Required lazily injected dependency + .add(Dependency.requiredProvider(MyOtherDep.class)) + .factory( c -> new MyComponent(c.get(MyDep.class), c.getProvider(MyOtherDep.class))) + .build(); +``` + +## Optional Dependencies + +This type of dependencies is useful when your `Component` can operate normally when the dependency is not +available, but can have enhanced functionality when present. e.g. `Firestore` can work without `Auth` but +provides secure database access when `Auth` is present. + +To declare an optional dependency use the following in your `ComponentRegistrar`: + +```java + .add(Dependency.optionalProvider(MyDep.class)) + .factory(c -> new MyComponent(c.getProvider(MyDep.class))) + .build(); +``` + +The provider will return `null` if the dependency is not present in the app. + +{: .warning } +When the app uses [Play's dynamic feature delivery](https://developer.android.com/guide/playcore/feature-delivery), +`provider.get()` will return your dependency when it becomes available. To support this use case, don't store references to the result of `provider.get()` calls. + +See [Dynamic Module Support]({{ site.baseurl }}{% link components/dynamic_modules.md %}) for details + +{: .warning } +See Deferred dependencies if you your dependency has a callback based API + +## Deferred Dependencies + +Useful for optional dependencies which have a listener-style API, i.e. the dependent component registers a +listener with the dependency and never calls it again (instead the dependency will call the registered listener). +A good example is `Firestore`'s use of `Auth`, where `Firestore` registers a token change listener to get +notified when a new token is available. The problem is that when `Firestore` initializes, `Auth` may not be +present in the app, and is instead part of a dynamic module that can be loaded at runtime on demand. + +To solve this problem, Components have a notion of a `Deferred` dependency. A deferred is defined as follows: + +```java +public interface Deferred { + interface DeferredHandler { + @DeferredApi + void handle(Provider provider); + } + + void whenAvailable(DeferredHandler handler); +} +``` + +To use it a component needs to call `Dependency.deferred(SomeType.class)`: + +```kotlin +class MyComponent(deferred: Deferred) { + init { + deferred.whenAvailable { someType -> + someType.registerListener(myListener) + } + } +} +``` + +See [Dynamic Module Support]({{ site.baseurl }}{% link components/dynamic_modules.md %}) for details + +## Set Dependencies + +The Components Framework allows registering components to be part of a set, such components are registered explicitly to be a part of a `Set` as opposed to be a unique value of `T`: + +```java +// Sdk 1 +Component.intoSet(new SomeTypeImpl(), SomeType.class); +// Sdk 2 +Component.intoSetBuilder(SomeType.class) + .add(Dependency(SomeDep.class)) + .factory(c -> new SomeOtherImpl(c.get(SomeDep.class))) + .build(); +``` + +With the above setup each SDK contributes a value of `SomeType` into a `Set` which becomes available as a +`Set` dependency. + +To consume such a set the interested `Component` needs to declare a special kind of dependency in one of 2 ways: + +* `Dependency.setOf(SomeType.class)`, a dependency of type `Set`. +* `Dependency.setOfProvider(SomeType.class)`, a dependency of type `Provider>`. The advantage of this + is that the `Set` is not initialized until the first call to `provider.get()` at which point all elements of the + set will get initialized. + +{: .warning } +Similar to optional `Provider` dependencies, where an optional dependency can become available at runtime due to +[Play's dynamic feature delivery](https://developer.android.com/guide/playcore/feature-delivery), +`Set` dependencies can change at runtime by new elements getting added to the set. +So make sure to hold on to the original `Set` to be able to observe new values in it as they are added. + +Example: + +```kotlin +class MyClass(private val set1: Set, private val set2: Provider>) +``` + +```java +Component.builder(MyClass.class) + .add(Dependency.setOf(SomeType.class)) + .add(Dependency.setOfProvider(SomeOtherType.class)) + .factory(c -> MyClass(c.setOf(SomeType.class), c.setOfProvider(SomeOtherType.class))) + .build(); +``` diff --git a/contributor-docs/components/dynamic_modules.md b/contributor-docs/components/dynamic_modules.md new file mode 100644 index 00000000000..a0d9cdb06e7 --- /dev/null +++ b/contributor-docs/components/dynamic_modules.md @@ -0,0 +1,7 @@ +--- +parent: Firebase Components +--- + +# Dynamic Module Support + +TODO diff --git a/contributor-docs/components/executors.md b/contributor-docs/components/executors.md new file mode 100644 index 00000000000..f5dde773a86 --- /dev/null +++ b/contributor-docs/components/executors.md @@ -0,0 +1,213 @@ +--- +parent: Firebase Components +--- + +# Executors +{: .no_toc} + +1. TOC +{:toc} + +## Intro + +OS threads are a limited resource that needs to be used with care. In order to minimize the number of threads used by Firebase +as a whole and to increase resource sharing Firebase Common provides a set of standard +[executors](https://developer.android.com/reference/java/util/concurrent/Executor) +and [coroutine dispatchers](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/) +for use by all Firebase SDKs. + +These executors are available as components and can be requested by product SDKs as component dependencies. + +Example: + +```java +public class MyRegistrar implements ComponentRegistrar { + public List> getComponents() { + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified liteExecutorService = Qualified.qualified(Lightweight.class, ExecutorService.class); + + return Collections.singletonList( + Component.builder(MyComponent.class) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(liteExecutorService)) + .factory(c -> new MyComponent(c.get(backgroundExecutor), c.get(liteExecutorService))) + .build()); + } +} +``` + +All executors(with the exception of `@UiThread`) are available as the following interfaces: + +* `Executor` +* `ExecutorService` +* `ScheduledExecutorService` +* `CoroutineDispatcher` + +`@UiThread` is provided only as a plain `Executor`. + +### Validation + +All SDKs have a custom linter check that detects creation of thread pools and threads, +this is to ensure SDKs use the above executors instead of creating their own. + +## Choose the right executor + +Use the following diagram to pick the right executor for the task you have at hand. + +```mermaid +flowchart TD + Start[Start] --> DoesBlock{Does it block?} + DoesBlock -->|No| NeedUi{Does it need to run\n on UI thread?} + NeedUi --> |Yes| UiExecutor[[UiThread Executor]] + NeedUi --> |No| TakesLong{Does it take more than\n 10ms to execute?} + TakesLong --> |No| LiteExecutor[[Lightweight Executor]] + TakesLong --> |Yes| BgExecutor[[Background Executor]] + DoesBlock --> |Yes| DiskIO{Does it block only\n on disk IO?} + DiskIO --> |Yes| BgExecutor + DiskIO --> |No| BlockExecutor[[Blocking Executor]] + + + classDef start fill:#4db6ac,stroke:#4db6ac,color:#000; + class Start start + + classDef condition fill:#f8f9fa,stroke:#bdc1c6,color:#000; + class DoesBlock condition; + class NeedUi condition; + class TakesLong condition; + class DiskIO condition; + + classDef executor fill:#1a73e8,stroke:#7baaf7,color:#fff; + class UiExecutor executor; + class LiteExecutor executor; + class BgExecutor executor; + class BlockExecutor executor; +``` + +### UiThread + +Used to schedule tasks on application's UI thread, internally it uses a Handler to post runnables onto the main looper. + +Example: + +```java +// Java +Qualified uiExecutor = qualified(UiThread.class, Executor.class); +``` + +```kotlin +// Kotlin +Qualified dispatcher = qualified(UiThread::class.java, CoroutineDispatcher::class.java); +``` + +### Lightweight + +Use for tasks that never block and don't take to long to execute. Backed by a thread pool of N threads +where N is the amount of parallelism available on the device(number of CPU cores) + +Example: + +```java +// Java +Qualified liteExecutor = qualified(Lightweight.class, Executor.class); +``` + +```kotlin +// Kotlin +Qualified dispatcher = qualified(Lightweight::class.java, CoroutineDispatcher::class.java); +``` + +### Background + +Use for tasks that may block on disk IO(use `@Blocking` for network IO or blocking on other threads). +Backed by 4 threads. + +Example: + +```java +// Java +Qualified bgExecutor = qualified(Background.class, Executor.class); +``` + +```kotlin +// Kotlin +Qualified dispatcher = qualified(Background::class.java, CoroutineDispatcher::class.java); +``` + +### Blocking + +Use for tasks that can block for arbitrary amounts of time, this includes network IO. + +Example: + +```java +// Java +Qualified blockingExecutor = qualified(Blocking.class, Executor.class); +``` + +```kotlin +// Kotlin +Qualified dispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java); +``` + +### Other executors + +#### Direct executor + +{: .warning } +Prefer `@Lightweight` instead of using direct executor as it could cause dead locks and stack overflows. + +For any trivial tasks that don't need to run asynchronously + +Example: + +```kotlin +FirebaseExecutors.directExecutor() +``` + +#### Sequential Executor + +When you need an executor that runs tasks sequentially and guarantees any memory access is synchronized prefer to use a sequential executor instead of creating a `newSingleThreadedExecutor()`. + +Example: + +```java +// Pick the appropriate underlying executor using the chart above +Qualified bgExecutor = qualified(Background.class, Executor.class); +// ... +Executor sequentialExecutor = FirebaseExecutors.newSequentialExecutor(c.get(bgExecutor)); +``` + +## Testing + +`@Lightweight` and `@Background` executors have StrictMode enabled and throw exceptions on violations. +For example trying to do Network IO on either of them will throw. +With that in mind, when it comes to writing tests, prefer to use the common executors as opposed to creating +your own thread pools. This will ensure that your code uses the appropriate executor and does not slow down +all of Firebase by using the wrong one. + +To do that, you should prefer relying on Components to inject the right executor even in tests. This will ensure +your tests are always using the executor that is actually used in your SDK build. +If your SDK uses Dagger, see [Dependency Injection]({{ site.baseurl }}{% link best_practices/dependency_injection.md %}) +and [Dagger's testing guide](https://dagger.dev/dev-guide/testing). + +When the above is not an option, you can use `TestOnlyExecutors`, but make sure you're testing your code with +the same executor that is used in production code: + +```kotlin +dependencies { + // ... + testImplementation(project(":integ-testing")) + // or + androidTestImplementation(project(":integ-testing")) +} + +``` + +This gives access to + +```java +TestOnlyExecutors.ui(); +TestOnlyExecutors.background(); +TestOnlyExecutors.blocking(); +TestOnlyExecutors.lite(); +``` diff --git a/contributor-docs/how_firebase_works.md b/contributor-docs/how_firebase_works.md new file mode 100644 index 00000000000..2424bda6b3c --- /dev/null +++ b/contributor-docs/how_firebase_works.md @@ -0,0 +1,85 @@ +--- +nav_order: 3 +--- + +# How Firebase Works + +## Background + +### Eager Initialization + +One of the biggest strengths for Firebase clients is the ease of integration. In a common case, a developer has very few things to do to integrate with Firebase. There is no need to initialize/configure Firebase at runtime. Firebase automatically initializes at application start and begins providing value to developers. A few notable examples: + +* `Analytics` automatically tracks app events +* `Firebase Performance` automatically tracks app startup time, all network requests and screen performance +* `Crashlytics` automatically captures all application crashes, ANRs and non-fatals + +This feature makes onboarding and adoption very simple. However, comes with the great responsibility of keeping the application snappy. We shouldn't slow down application startup for 3p developers as it can stand in the way of user adoption of their application. + +### Automatic Inter-Product Discovery + +When present together in an application, Firebase products can detect each other and automatically provide additional functionality to the developer, e.g.: + +* `Firestore` automatically detects `Auth` and `AppCheck` to protect read/write access to the database +* `Crashlytics` integrates with `Analytics`, when available, to provide additional insights into the application behavior and enables safe app rollouts + +## FirebaseApp at the Core of Firebase + +Regardless of what Firebase SDKs are present in the app, the main initialization point of Firebase is `FirebaseApp`. It acts as a container for all SDKs, manages their configuration, initialization and lifecycle. + +### Initialization + +`FirebaseApp` gets initialized with the help of `FirebaseApp#initializeApp()`. This happens [automatically at app startup](https://firebase.blog/posts/2016/12/how-does-firebase-initialize-on-android) or manually by the developer. + +During initialization, `FirebaseApp` discovers all Firebase SDKs present in the app, determines the dependency graph between products(for inter-product functionality) and initializes `eager` products that need to start immediately, e.g. `Crashlytics` and `FirebasePerformance`. + +### Firebase Configuration + +`FirebaseApp` contains Firebase configuration for all products to use, namely `FirebaseOptions`, which tells Firebase which `Firebase` project to talk to, which real-time database to use, etc. + +### Additional Services/Components + +In addition to `FirebaseOptions`, `FirebaseApp` registers additional components that product SDKs can request via dependency injection. To name a few: + +* `android.content.Context`(Application context) +* [Common Executors]({{ site.baseurl }}{% link components/executors.md %}) +* `FirebaseOptions` +* Various internal components + +## Discovery and Dependency Injection + +There are multiple considerations that lead to the current design of how Firebase SDKs initialize. + +1. Certain SDKs need to initialize at app startup. +2. SDKs have optional dependencies on other products that get enabled when the developer adds the dependency to their app. + +To enable this functionality, Firebase uses a runtime discovery and dependency injection framework [firebase-components](https://github.com/firebase/firebase-android-sdk/tree/master/firebase-components). + +To integrate with this framework SDKs register the components they provide via a `ComponentRegistrar` and declare any dependencies they need to initialize, e.g. + +```java +public class MyRegistrar implements ComponentRegistrar { + @Override + public List> getComponents() { + return Arrays.asList( + // declare the component + Component.builder(MyComponent.class) + // declare dependencies + .add(Dependency.required(Context.class)) + .add(Dependency.required(FirebaseOptions.class)) + .add(Dependency.optionalProvider(InternalAuthProvider.class)) + // let the runtime know how to create your component. + .factory( + diContainer -> + new MyComponent( + diContainer.get(Context.class), + diContainer.get(FirebaseOptions.class), + diContainer.get(InternalAuthProvider.class))) + .build()); + } +} +``` + +This registrar is then registered in `AndroidManifest.xml` of the SDK and is used by `FirebaseApp` to discover all components and construct the dependency graph. + +More details in [Firebase Components]({{ site.baseurl }}{% link components/components.md %}). diff --git a/contributor-docs/onboarding/as_open_project.png b/contributor-docs/onboarding/as_open_project.png new file mode 100644 index 00000000000..543f4a31090 Binary files /dev/null and b/contributor-docs/onboarding/as_open_project.png differ diff --git a/contributor-docs/onboarding/env_setup.md b/contributor-docs/onboarding/env_setup.md new file mode 100644 index 00000000000..95427f8a66a --- /dev/null +++ b/contributor-docs/onboarding/env_setup.md @@ -0,0 +1,50 @@ +--- +parent: Onboarding +--- + +# Development Environment Setup + +This page describes software and configuration required to work on code in the +[Firebase/firebase-android-sdk](https://github.com/firebase/firebase-android-sdk) +repository. + +{:toc} + +## JDK + +The currently required version of the JDK is `11`. Any other versions are +unsupported and using them could result in build failures. + +## Android Studio + +In general, the most recent version of Android Studio should work. The version +that is tested at the time of this writing is `Dolphin | 2021.3.1`. + +Download it here: +[Download Android Studio](https://developer.android.com/studio) + +## Emulators + +If you plan to run tests on emulators(you should), you should be able to install +them directly from Android Studio's AVD manager. + +## Github (Googlers Only) + +To onboard and get write access to the github repository you need to have a +github account fully linked with [go/github](http://go/github). + +File a bug using this +[bug template](http://b/issues/new?component=312729&template=1016566) and wait +for access to be granted. + +After that configure github keys as usual using this +[Github documentation page](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh). + +## Importing the repository + +1. Clone the repository with `git clone --recurse-submodules + git@github.com:firebase/firebase-android-sdk.git`. +1. Open Android Studio and click "Open an existing project". + ![Open an existing project](as_open_project.png) +1. Find the `firebase-android-sdk` directory and open. +1. To run integration/device tests you will need a `google-services.json` file. diff --git a/contributor-docs/onboarding/new_sdk.md b/contributor-docs/onboarding/new_sdk.md new file mode 100644 index 00000000000..a922741fb3b --- /dev/null +++ b/contributor-docs/onboarding/new_sdk.md @@ -0,0 +1,227 @@ +--- +parent: Onboarding +--- + +# Creating a new Firebase SDK +{: .no_toc} + +1. TOC +{:toc} + +Want to create a new SDK in +[firebase/firebase-android-sdk](https://github.com/firebase/firebase-android-sdk)? +Read on. + +{:toc} + +## Repository layout and Gradle + +[firebase/firebase-android-sdk](https://github.com/firebase/firebase-android-sdk) +uses a multi-project Gradle build to organize the different libraries it hosts. +As a consequence, each project/product within this repo is hosted under its own +subdirectory with its respective build file(s). + +```bash +firebase-android-sdk +├── buildSrc +├── appcheck +│ └── firebase-appcheck +│ └── firebase-appcheck-playintegrity +├── firebase-annotations +├── firebase-common +│ └── firebase-common.gradle # note the name of the build file +│ └── ktx +│ └── ktx.gradle.kts # it can also be kts +└── build.gradle # root project build file. +``` + +Most commonly, SDKs are located as immediate child directories of the root +directory, with the directory name being the exact name of the Maven artifact ID +the library will have once released. e.g. `firebase-common` directory +hosts code for the `com.google.firebase:firebase-common` SDK. + +{: .warning } +Note that the build file name for any given SDK is not `build.gradle` or `build.gradle.kts` +but rather mirrors the name of the sdk, e.g. +`firebase-common/firebase-common.gradle` or `firebase-common/firebase-common.gradle.kts`. + +All of the core Gradle build logic lives in `buildSrc` and is used by all +SDKs. + +SDKs can be grouped together for convenience by placing them in a directory of +choice. + +## Creating an SDK + +Let's say you want to create an SDK named `firebase-foo` + +1. Create a directory called `firebase-foo`. +1. Create a file `firebase-foo/firebase-foo.gradle.kts`. +1. Add `firebase-foo` line to `subprojects.cfg` at the root of the tree. + +### Update `firebase-foo.gradle.kts` with the following content + +
+ + firebase-foo.gradle.kts + +```kotlin +plugins { + id("firebase-library") + // Uncomment for Kotlin + // id("kotlin-android") +} + +firebaseLibrary { + // enable this only if you have tests in `androidTest`. + testLab.enabled = true + publishSources = true + publishJavadoc = true +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + + compileSdk = targetSdkVersion + defaultConfig { + namespace = "com.google.firebase.foo" + // change this if you have custom needs. + minSdk = minSdkVersion + targetSdk = targetSdkVersion + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) +} + +``` +
+ +### Create `src/main/AndroidManifest.xml` with the following content: + +
+ + src/main/AndroidManifest.xml + +```xml + + + + + + + + + + + + + + + + + + + + + + +``` + +
+ +### Create `com.google.firebase.foo.FirebaseFoo` + +For Kotlin +
+ + src/main/kotlin/com/google/firebase/foo/FirebaseFoo.kt + + +```kotlin +class FirebaseFoo { + companion object { + @JvmStatic + val instance: FirebaseFoo + get() = getInstance(Firebase.app) + + @JvmStatic fun getInstance(app: FirebaseApp): FirebaseFoo = app.get(FirebaseFoo::class.java) + } +} +``` + +
+ +For Java +
+ + src/main/java/com/google/firebase/foo/FirebaseFoo.java + + +```java +public class FirebaseFoo { + public static FirebaseFoo getInstance() { + return getInstance(FirebaseApp.getInstance()); + } + public static FirebaseFoo getInstance(FirebaseApp app) { + return app.get(FirebaseFoo.class); + } +} +``` + +
+ +### Create `com.google.firebase.foo.FirebaseFooRegistrar` + +For Kotlin +
+ + src/main/kotlin/com/google/firebase/foo/FirebaseFooRegistrar.kt + + +{: .warning } +You should strongly consider using [Dependency Injection]({{ site.baseurl }}{% link best_practices/dependency_injection.md %}) +to instantiate your sdk instead of manually constructing its instance in the `factory()` below. + +```kotlin +class FirebaseFooRegistrar : ComponentRegistrar { + override fun getComponents() = + listOf( + Component.builder(FirebaseFoo::class.java).factory { container -> FirebaseFoo() }.build(), + LibraryVersionComponent.create("fire-foo", BuildConfig.VERSION_NAME) + ) +} +``` + +
+ +For Java +
+ + src/main/java/com/google/firebase/foo/FirebaseFooRegistrar.java + + +```java +public class FirebaseFooRegistrar implements ComponentRegistrar { + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder(FirebaseFoo.class).factory(c -> new FirebaseFoo()).build(), + LibraryVersionComponent.create("fire-foo", BuildConfig.VERSION_NAME)); + + } +} +``` + +
+ +Continue to [How Firebase works]({{ site.baseurl }}{% link how_firebase_works.md %}). diff --git a/contributor-docs/onboarding/onboarding.md b/contributor-docs/onboarding/onboarding.md new file mode 100644 index 00000000000..b7291dde2bc --- /dev/null +++ b/contributor-docs/onboarding/onboarding.md @@ -0,0 +1,7 @@ +--- +has_children: true +permalink: /onboarding/ +nav_order: 2 +--- + +# Onboarding diff --git a/docs/executors.md b/docs/executors.md new file mode 100644 index 00000000000..8cf1d423405 --- /dev/null +++ b/docs/executors.md @@ -0,0 +1,3 @@ +# Executors + +Moved to [This page](https://firebase.github.io/firebase-android-sdk/components/executors). diff --git a/docs/ktx/firestore.md b/docs/ktx/firestore.md index d50ab83b8b6..276f6bc0820 100644 --- a/docs/ktx/firestore.md +++ b/docs/ktx/firestore.md @@ -27,6 +27,64 @@ val firestore = Firebase.firestore val anotherFirestore = Firebase.firestore(Firebase.app("myApp")) ``` +### Get a document + +**Kotlin** +```kotlin +firestore.collection("cities") + .document("LON") + .addSnapshotListener { document: DocumentSnapshot?, error: -> + if (error != null) { + // Handle error + return@addSnapshotListener + } + if (document != null) { + // Use document + } + } +``` + +**Kotlin + KTX** +```kotlin +firestore.collection("cities") + .document("LON") + .snapshots() + .collect { document: DocumentSnapshot -> + // Use document + } +``` + +### Query documents + +**Kotlin** +```kotlin +firestore.collection("cities") + .whereEqualTo("capital", true) + .addSnapshotListener { documents: QuerySnapshot?, error -> + if (error != null) { + // Handle error + return@addSnapshotListener + } + if (documents != null) { + for (document in documents) { + // Use document + } + } + } +``` + +**Kotlin + KTX** +```kotlin +firestore.collection("cities") + .whereEqualTo("capital", true) + .snapshots() + .collect { documents: QuerySnapshot -> + for (document in documents) { + // Use document + } + } +``` + ### Convert a DocumentSnapshot field to a POJO **Kotlin** diff --git a/encoders/protoc-gen-firebase-encoders/protoc-gen-firebase-encoders.gradle b/encoders/protoc-gen-firebase-encoders/protoc-gen-firebase-encoders.gradle index 380795c72ce..3c3a1cd59a7 100644 --- a/encoders/protoc-gen-firebase-encoders/protoc-gen-firebase-encoders.gradle +++ b/encoders/protoc-gen-firebase-encoders/protoc-gen-firebase-encoders.gradle @@ -37,7 +37,7 @@ jar { dependencies { - implementation "com.google.protobuf:protobuf-java:3.14.0" + implementation "com.google.protobuf:protobuf-java:3.21.9" implementation 'com.squareup:javapoet:1.13.0' implementation 'com.google.guava:guava:30.0-jre' implementation 'com.google.dagger:dagger:2.43.2' diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/CodeGenerator.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/CodeGenerator.kt index ac08deb749b..c5ea98e5a89 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/CodeGenerator.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/CodeGenerator.kt @@ -32,459 +32,523 @@ import java.io.OutputStream import javax.inject.Inject import javax.lang.model.element.Modifier -private val ENCODER_CLASS = ClassName.get( - "com.google.firebase.encoders.proto", "ProtobufEncoder") +private val ENCODER_CLASS = ClassName.get("com.google.firebase.encoders.proto", "ProtobufEncoder") private val ENCODABLE_ANNOTATION = - ClassName.get("com.google.firebase.encoders.annotations", "Encodable") -private val PROTOBUF_ANNOTATION = ClassName.get( - "com.google.firebase.encoders.proto", "Protobuf") + ClassName.get("com.google.firebase.encoders.annotations", "Encodable") +private val PROTOBUF_ANNOTATION = ClassName.get("com.google.firebase.encoders.proto", "Protobuf") private val FIELD_ANNOTATION = ENCODABLE_ANNOTATION.nestedClass("Field") private val IGNORE_ANNOTATION = ENCODABLE_ANNOTATION.nestedClass("Ignore") internal class Gen( - private val messages: Collection, - private val vendorPackage: String + private val messages: Collection, + private val vendorPackage: String ) { - private val rootEncoder = ClassName.get(vendorPackage, "ProtoEncoderDoNotUse") - private val index = mutableMapOf() - val rootClasses = mutableMapOf() + private val rootEncoder = ClassName.get(vendorPackage, "ProtoEncoderDoNotUse") + private val index = mutableMapOf() + val rootClasses = mutableMapOf() - fun generate() { - if (messages.isEmpty()) { - return - } - - rootClasses[Message( - owner = Owner.Package(vendorPackage, vendorPackage, "unknown"), - name = "ProtoEncoderDoNotUse", - fields = listOf())] = protoEncoder() + fun generate() { + if (messages.isEmpty()) { + return + } - for (message in messages) { - generate(message) - } + rootClasses[ + Message( + owner = Owner.Package(vendorPackage, vendorPackage, "unknown"), + name = "ProtoEncoderDoNotUse", + fields = listOf() + )] = protoEncoder() - // Messages generated above are "shallow". Meaning that they don't include their nested - // message types. Below we are addressing that by nesting messages within their respective - // containing classes. - for (type in index.keys) { - (type.owner as? Owner.MsgRef)?.let { - index[it.message]!!.addType(index[type]!!.build()) - } - } + for (message in messages) { + generate(message) } - /** - * Generates the "root" `@Encodable` class. - * - * This class is the only `@Encodable`-annotated class in the codegen. This ensures that we - * generate an encoder exactly once per message and there is no code duplication. - * - * All "included" messages(as per plugin config) delegate to this class to implement encoding. - */ - private fun protoEncoder(): TypeSpec.Builder { - return TypeSpec.classBuilder(rootEncoder) - .addAnnotation(ENCODABLE_ANNOTATION) - .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) - .addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .build()) - .addField(FieldSpec.builder( - ENCODER_CLASS, - "ENCODER", - Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) - .initializer("\$T.builder().configureWith(AutoProtoEncoderDoNotUseEncoder.CONFIG).build()", ENCODER_CLASS) - .build()) - .addMethod(MethodSpec.methodBuilder("encode") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ArrayTypeName.of(TypeName.BYTE)) - .addParameter(Any::class.java, "value") - .addCode("return ENCODER.encode(value);\n") - .build()) - .addMethod(MethodSpec.methodBuilder("encode") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addException(IOException::class.java) - .addParameter(Any::class.java, "value") - .addParameter(OutputStream::class.java, "output") - .addCode("ENCODER.encode(value, output);\n") - .build()) - .apply { - for (message in messages) { - addMethod(MethodSpec.methodBuilder("get${message.name.capitalize()}") - .returns(message.toTypeName()) - .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) - .build()) - } - } + // Messages generated above are "shallow". Meaning that they don't include their nested + // message types. Below we are addressing that by nesting messages within their respective + // containing classes. + for (type in index.keys) { + (type.owner as? Owner.MsgRef)?.let { index[it.message]!!.addType(index[type]!!.build()) } } + } + + /** + * Generates the "root" `@Encodable` class. + * + * This class is the only `@Encodable`-annotated class in the codegen. This ensures that we + * generate an encoder exactly once per message and there is no code duplication. + * + * All "included" messages(as per plugin config) delegate to this class to implement encoding. + */ + private fun protoEncoder(): TypeSpec.Builder { + return TypeSpec.classBuilder(rootEncoder) + .addAnnotation(ENCODABLE_ANNOTATION) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()) + .addField( + FieldSpec.builder( + ENCODER_CLASS, + "ENCODER", + Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL + ) + .initializer( + "\$T.builder().configureWith(AutoProtoEncoderDoNotUseEncoder.CONFIG).build()", + ENCODER_CLASS + ) + .build() + ) + .addMethod( + MethodSpec.methodBuilder("encode") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ArrayTypeName.of(TypeName.BYTE)) + .addParameter(Any::class.java, "value") + .addCode("return ENCODER.encode(value);\n") + .build() + ) + .addMethod( + MethodSpec.methodBuilder("encode") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addException(IOException::class.java) + .addParameter(Any::class.java, "value") + .addParameter(OutputStream::class.java, "output") + .addCode("ENCODER.encode(value, output);\n") + .build() + ) + .apply { + for (message in messages) { + addMethod( + MethodSpec.methodBuilder("get${message.name.capitalize()}") + .returns(message.toTypeName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .build() + ) + } + } + } - private fun generate(type: UserDefined) { - if (type in index) return + private fun generate(type: UserDefined) { + if (type in index) return - index[type] = TypeSpec.classBuilder("Dummy") + index[type] = TypeSpec.classBuilder("Dummy") - val builder = when (type) { - is ProtoEnum -> generateEnum(type) - is Message -> { - val classBuilder = generateClass(type) - classBuilder.addType(generateBuilder(type)) + val builder = + when (type) { + is ProtoEnum -> generateEnum(type) + is Message -> { + val classBuilder = generateClass(type) + classBuilder.addType(generateBuilder(type)) - for (field in type.fields) { - (field.type as? UserDefined)?.let { - generate(it) - } - } - classBuilder - } + for (field in type.fields) { + (field.type as? UserDefined)?.let { generate(it) } + } + classBuilder } - if (type.owner is Owner.Package) { - rootClasses[type] = builder - } - index[type] = builder + } + if (type.owner is Owner.Package) { + rootClasses[type] = builder } - - /** - * Generates an enum. - * - * Example generated enum: - * - * ```java - * import com.google.firebase.encoders.proto.ProtoEnum; - * - * public enum Foo implements ProtoEnum { - * DEFAULT(0), - * EXAMPLE(1); - * - * private final int number_; - * Foo(int number_) { - * this.number_ = number_; - * } - * - * @Override - * public int getNumber() { - * return number_; - * } - * } - * ``` - */ - private fun generateEnum(type: ProtoEnum): TypeSpec.Builder { - val builder = TypeSpec.enumBuilder(type.name).apply { - addModifiers(Modifier.PUBLIC) - this.addSuperinterface( - ClassName.get("com.google.firebase.encoders.proto", "ProtoEnum")) - for (value in type.values) { - addEnumConstant("${value.name}(${value.value})") - } + index[type] = builder + } + + /** + * Generates an enum. + * + * Example generated enum: + * + * ```java + * import com.google.firebase.encoders.proto.ProtoEnum; + * + * public enum Foo implements ProtoEnum { + * DEFAULT(0), + * EXAMPLE(1); + * + * private final int number_; + * Foo(int number_) { + * this.number_ = number_; + * } + * + * @Override + * public int getNumber() { + * return number_; + * } + * } + * ``` + */ + private fun generateEnum(type: ProtoEnum): TypeSpec.Builder { + val builder = + TypeSpec.enumBuilder(type.name).apply { + addModifiers(Modifier.PUBLIC) + this.addSuperinterface(ClassName.get("com.google.firebase.encoders.proto", "ProtoEnum")) + for (value in type.values) { + addEnumConstant("${value.name}(${value.value})") } - builder.addField(FieldSpec.builder(TypeName.INT, "number_", Modifier.PRIVATE, Modifier.FINAL).build()) - builder.addMethod(MethodSpec.constructorBuilder() - .addModifiers(Modifier.PRIVATE) - .addParameter(TypeName.INT, "number_") - .addCode("this.number_ = number_;\n").build()) - builder.addMethod(MethodSpec.methodBuilder("getNumber") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override::class.java) - .returns(TypeName.INT) - .addCode("return number_;\n") - .build()) - return builder + } + builder.addField( + FieldSpec.builder(TypeName.INT, "number_", Modifier.PRIVATE, Modifier.FINAL).build() + ) + builder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(TypeName.INT, "number_") + .addCode("this.number_ = number_;\n") + .build() + ) + builder.addMethod( + MethodSpec.methodBuilder("getNumber") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override::class.java) + .returns(TypeName.INT) + .addCode("return number_;\n") + .build() + ) + return builder + } + + /** + * Generates a class that corresponds to a proto message. + * + * Example class: + * ```java + * public final class Foo { + * private final int field_; + * private final List str_; + * private Foo(int field_, List str_) { + * this.field_ = field_; + * this.str_ = str_; + * } + * + * @Protobuf(tag = 1) + * public int getField() { return field_; } + * + * @Protobuf(tag = 2) + * @Field(name = "str") + * public List getStrList() { return field_; } + * + * // see generateBuilder() below + * public static class Builder {} + * public static Builder newBuilder() ; + * + * public static Foo getDefaultInstance(); + * + * // these are generated only for types explicitly specified in the plugin config. + * public void writeTo(OutputStream output) throws IOException; + * public byte[] toByteArray(); + * + * } + * ``` + */ + // TODO(vkryachko): generate equals() and hashCode() + private fun generateClass(type: Message): TypeSpec.Builder { + val messageClass = + if (type.owner is Owner.Package) { + TypeSpec.classBuilder(type.name).addModifiers(Modifier.PUBLIC, Modifier.FINAL) + } else { + TypeSpec.classBuilder(type.name) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) + } + if (type in messages) { + messageClass + .addMethod( + MethodSpec.methodBuilder("toByteArray") + .addModifiers(Modifier.PUBLIC) + .returns(ArrayTypeName.of(TypeName.BYTE)) + .addCode("return \$T.encode(this);\n", rootEncoder) + .build() + ) + .addMethod( + MethodSpec.methodBuilder("writeTo") + .addModifiers(Modifier.PUBLIC) + .addException(IOException::class.java) + .addParameter(OutputStream::class.java, "output") + .addCode("\$T.encode(this, output);\n", rootEncoder) + .build() + ) } - - /** - * Generates a class that corresponds to a proto message. - * - * Example class: - * ```java - * public final class Foo { - * private final int field_; - * private final List str_; - * private Foo(int field_, List str_) { - * this.field_ = field_; - * this.str_ = str_; - * } - * - * @Protobuf(tag = 1) - * public int getField() { return field_; } - * - * @Protobuf(tag = 2) - * @Field(name = "str") - * public List getStrList() { return field_; } - * - * // see generateBuilder() below - * public static class Builder {} - * public static Builder newBuilder() ; - * - * public static Foo getDefaultInstance(); - * - * // these are generated only for types explicitly specified in the plugin config. - * public void writeTo(OutputStream output) throws IOException; - * public byte[] toByteArray(); - * - * } - * ``` - */ - // TODO(vkryachko): generate equals() and hashCode() - private fun generateClass(type: Message): TypeSpec.Builder { - val messageClass = if (type.owner is Owner.Package) { - TypeSpec.classBuilder(type.name).addModifiers(Modifier.PUBLIC, Modifier.FINAL) - } else { - TypeSpec.classBuilder(type.name).addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) - } - if (type in messages) { - messageClass - .addMethod(MethodSpec.methodBuilder("toByteArray") - .addModifiers(Modifier.PUBLIC) - .returns(ArrayTypeName.of(TypeName.BYTE)) - .addCode("return \$T.encode(this);\n", rootEncoder) - .build()) - .addMethod(MethodSpec.methodBuilder("writeTo") - .addModifiers(Modifier.PUBLIC) - .addException(IOException::class.java) - .addParameter(OutputStream::class.java, "output") - .addCode("\$T.encode(this, output);\n", rootEncoder) - .build()) - } - val messageTypeName = ClassName.bestGuess(type.name) - - val constructor = MethodSpec.constructorBuilder() - messageClass.addMethod(MethodSpec.methodBuilder("newBuilder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(ClassName.bestGuess("Builder")) - .addCode("return new Builder();\n") - .build()) - - for (field in type.fields) { - messageClass.addField(FieldSpec.builder(field.typeName, "${field.name}_", Modifier.PRIVATE, Modifier.FINAL).build()) - constructor.addParameter(field.typeName, "${field.name}_") - constructor.addCode("this.${field.name}_ = ${field.name}_;\n") - - if (field.repeated || field.type !is Message) { - messageClass.addMethod(MethodSpec.methodBuilder("get${field.camelCaseName}${if (field.repeated) "List" else ""}") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(AnnotationSpec.builder(PROTOBUF_ANNOTATION) - .addMember("tag", "${field.number}") - .apply { - field.type.intEncoding?.let { - addMember("intEncoding", it) - } - } - .build()) - .apply { - if (field.repeated) { - addAnnotation(AnnotationSpec.builder(FIELD_ANNOTATION) - .addMember("name", "\$S", field.lowerCamelCaseName) - .build()) - } - } - .returns(field.typeName) - .addCode("return ${field.name}_;\n") - .build()) - } else { - // this method is for use as public API, it never returns null and falls back to - // returning default instances. - messageClass.addMethod(MethodSpec.methodBuilder("get${field.camelCaseName}") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(AnnotationSpec.builder(IGNORE_ANNOTATION).build()) - .returns(field.typeName) - .addCode("return ${field.name}_ == null ? \$T.getDefaultInstance() : ${field.name}_;\n", field.typeName) - .build()) - // this method can return null and is needed by the encoder to make sure we don't - // try to encode default instances, which is: - // 1. inefficient - // 2. can lead to infinite recursion in case of self-referential types. - messageClass.addMethod(MethodSpec.methodBuilder("get${field.camelCaseName}Internal") - .addModifiers(Modifier.PUBLIC) - .addAnnotation(AnnotationSpec.builder(PROTOBUF_ANNOTATION) - .addMember("tag", "${field.number}") - .build()) - .addAnnotation(AnnotationSpec.builder(FIELD_ANNOTATION) - .addMember("name", "\$S", field.lowerCamelCaseName) - .build()) - .returns(field.typeName) - .addCode("return ${field.name}_;\n") - .build()) + val messageTypeName = ClassName.bestGuess(type.name) + + val constructor = MethodSpec.constructorBuilder() + messageClass.addMethod( + MethodSpec.methodBuilder("newBuilder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.bestGuess("Builder")) + .addCode("return new Builder();\n") + .build() + ) + + for (field in type.fields) { + messageClass.addField( + FieldSpec.builder(field.typeName, "${field.name}_", Modifier.PRIVATE, Modifier.FINAL) + .build() + ) + constructor.addParameter(field.typeName, "${field.name}_") + constructor.addCode("this.${field.name}_ = ${field.name}_;\n") + + if (field.repeated || field.type !is Message) { + messageClass.addMethod( + MethodSpec.methodBuilder("get${field.camelCaseName}${if (field.repeated) "List" else ""}") + .addModifiers(Modifier.PUBLIC) + .addAnnotation( + AnnotationSpec.builder(PROTOBUF_ANNOTATION) + .addMember("tag", "${field.number}") + .apply { field.type.intEncoding?.let { addMember("intEncoding", it) } } + .build() + ) + .apply { + if (field.repeated) { + addAnnotation( + AnnotationSpec.builder(FIELD_ANNOTATION) + .addMember("name", "\$S", field.lowerCamelCaseName) + .build() + ) + } } - } - messageClass.addMethod(constructor.build()) - messageClass.addField(FieldSpec.builder( - messageTypeName, - "DEFAULT_INSTANCE", - Modifier.PRIVATE, - Modifier.STATIC, - Modifier.FINAL) - .initializer("new Builder().build()", messageTypeName) - .build()) - messageClass.addMethod(MethodSpec.methodBuilder("getDefaultInstance") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .returns(messageTypeName) - .addCode("return DEFAULT_INSTANCE;\n").build()) - return messageClass + .returns(field.typeName) + .addCode("return ${field.name}_;\n") + .build() + ) + } else { + // this method is for use as public API, it never returns null and falls back to + // returning default instances. + messageClass.addMethod( + MethodSpec.methodBuilder("get${field.camelCaseName}") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(AnnotationSpec.builder(IGNORE_ANNOTATION).build()) + .returns(field.typeName) + .addCode( + "return ${field.name}_ == null ? \$T.getDefaultInstance() : ${field.name}_;\n", + field.typeName + ) + .build() + ) + // this method can return null and is needed by the encoder to make sure we don't + // try to encode default instances, which is: + // 1. inefficient + // 2. can lead to infinite recursion in case of self-referential types. + messageClass.addMethod( + MethodSpec.methodBuilder("get${field.camelCaseName}Internal") + .addModifiers(Modifier.PUBLIC) + .addAnnotation( + AnnotationSpec.builder(PROTOBUF_ANNOTATION) + .addMember("tag", "${field.number}") + .build() + ) + .addAnnotation( + AnnotationSpec.builder(FIELD_ANNOTATION) + .addMember("name", "\$S", field.lowerCamelCaseName) + .build() + ) + .returns(field.typeName) + .addCode("return ${field.name}_;\n") + .build() + ) + } } - - /** - * Generates a builder for a proto message. - * - * Example builder: - * ```java - * public static final class Builder { - * public Foo build(); - * public Builder setField(int value); - * public Builder setStrList(List value); - * public Builder addStr(String value); - * } - * ``` - */ - // TODO(vkryachko): oneof setters should clear other oneof cases. - private fun generateBuilder(type: Message): TypeSpec { - val messageTypeName = ClassName.bestGuess(type.name) - - val builder = TypeSpec.classBuilder("Builder") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) - - val buildMethodArgs = type.fields.joinToString(", ") { - if (it.repeated) "java.util.Collections.unmodifiableList(${it.name}_)" - else "${it.name}_" - } - builder.addMethod(MethodSpec.methodBuilder("build") - .addModifiers(Modifier.PUBLIC) - .returns(messageTypeName) - .addCode("return new \$T(\$L);\n", messageTypeName, buildMethodArgs) - .build()) - - val builderConstructor = MethodSpec.constructorBuilder() - - for (field in type.fields) { - builder.addField(FieldSpec.builder(field.typeName, "${field.name}_", Modifier.PRIVATE).build()) - builderConstructor.addCode("this.${field.name}_ = ").apply { - field.type.let { t -> - when (t) { - is Message -> when { - field.repeated -> addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) - else -> addCode("null") - } - is Primitive -> - if (field.repeated) - addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) - else - addCode(t.defaultValue) - is ProtoEnum -> - if (field.repeated) - addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) - else - addCode(t.defaultValue.replace("$", ".")) - is Unresolved -> TODO() - } + messageClass.addMethod(constructor.build()) + messageClass.addField( + FieldSpec.builder( + messageTypeName, + "DEFAULT_INSTANCE", + Modifier.PRIVATE, + Modifier.STATIC, + Modifier.FINAL + ) + .initializer("new Builder().build()", messageTypeName) + .build() + ) + messageClass.addMethod( + MethodSpec.methodBuilder("getDefaultInstance") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(messageTypeName) + .addCode("return DEFAULT_INSTANCE;\n") + .build() + ) + return messageClass + } + + /** + * Generates a builder for a proto message. + * + * Example builder: + * ```java + * public static final class Builder { + * public Foo build(); + * public Builder setField(int value); + * public Builder setStrList(List value); + * public Builder addStr(String value); + * } + * ``` + */ + // TODO(vkryachko): oneof setters should clear other oneof cases. + private fun generateBuilder(type: Message): TypeSpec { + val messageTypeName = ClassName.bestGuess(type.name) + + val builder = + TypeSpec.classBuilder("Builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + + val buildMethodArgs = + type.fields.joinToString(", ") { + if (it.repeated) "java.util.Collections.unmodifiableList(${it.name}_)" else "${it.name}_" + } + builder.addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(messageTypeName) + .addCode("return new \$T(\$L);\n", messageTypeName, buildMethodArgs) + .build() + ) + + val builderConstructor = MethodSpec.constructorBuilder() + + for (field in type.fields) { + builder.addField( + FieldSpec.builder(field.typeName, "${field.name}_", Modifier.PRIVATE).build() + ) + builderConstructor + .addCode("this.${field.name}_ = ") + .apply { + field.type.let { t -> + when (t) { + is Message -> + when { + field.repeated -> addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) + else -> addCode("null") } - }.addCode(";\n") - if (field.repeated) { - builder.addMethod(MethodSpec.methodBuilder("add${field.camelCaseName}") - .addModifiers(Modifier.PUBLIC) - .returns(ClassName.bestGuess("Builder")) - .addParameter(field.type.toTypeName(), "${field.name}_") - .addCode("this.${field.name}_.add(${field.name}_);\n") - .addCode("return this;\n") - .build()) + is Primitive -> + if (field.repeated) addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) + else addCode(t.defaultValue) + is ProtoEnum -> + if (field.repeated) addCode("new \$T<>()", ClassName.get(ArrayList::class.java)) + else addCode(t.defaultValue.replace("$", ".")) + is Unresolved -> TODO() } - builder.addMethod(MethodSpec.methodBuilder("set${field.camelCaseName}${if (field.repeated) "List" else ""}") - .addModifiers(Modifier.PUBLIC) - .returns(ClassName.bestGuess("Builder")) - .addParameter(field.typeName, "${field.name}_") - .addCode("this.${field.name}_ = ${field.name}_;\n") - .addCode("return this;\n") - .build()) + } } - builder.addMethod(builderConstructor.build()) - return builder.build() + .addCode(";\n") + if (field.repeated) { + builder.addMethod( + MethodSpec.methodBuilder("add${field.camelCaseName}") + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.bestGuess("Builder")) + .addParameter(field.type.toTypeName(), "${field.name}_") + .addCode("this.${field.name}_.add(${field.name}_);\n") + .addCode("return this;\n") + .build() + ) + } + builder.addMethod( + MethodSpec.methodBuilder("set${field.camelCaseName}${if (field.repeated) "List" else ""}") + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.bestGuess("Builder")) + .addParameter(field.typeName, "${field.name}_") + .addCode("this.${field.name}_ = ${field.name}_;\n") + .addCode("return this;\n") + .build() + ) } + builder.addMethod(builderConstructor.build()) + return builder.build() + } } private fun ProtobufType.toTypeName(): TypeName { - return when (this) { - Primitive.INT32 -> TypeName.INT - Primitive.SINT32 -> TypeName.INT - Primitive.FIXED32 -> TypeName.INT - Primitive.SFIXED32 -> TypeName.INT - Primitive.FLOAT -> TypeName.FLOAT - Primitive.INT64 -> TypeName.LONG - Primitive.SINT64 -> TypeName.LONG - Primitive.FIXED64 -> TypeName.LONG - Primitive.SFIXED64 -> TypeName.LONG - Primitive.DOUBLE -> TypeName.DOUBLE - Primitive.BOOLEAN -> TypeName.BOOLEAN - Primitive.STRING -> ClassName.get(String::class.java) - Primitive.BYTES -> ArrayTypeName.of(TypeName.BYTE) - is Message -> { - return when (owner) { - is Owner.Package -> ClassName.get(owner.javaName, name) - is Owner.MsgRef -> (owner.message.toTypeName() as ClassName).nestedClass(name) - } - } - is ProtoEnum -> { - return when (owner) { - is Owner.Package -> ClassName.get(owner.javaName, name) - is Owner.MsgRef -> (owner.message.toTypeName() as ClassName).nestedClass(name) - } - } - is Unresolved -> throw AssertionError("Impossible!") + return when (this) { + Primitive.INT32 -> TypeName.INT + Primitive.SINT32 -> TypeName.INT + Primitive.FIXED32 -> TypeName.INT + Primitive.SFIXED32 -> TypeName.INT + Primitive.FLOAT -> TypeName.FLOAT + Primitive.INT64 -> TypeName.LONG + Primitive.SINT64 -> TypeName.LONG + Primitive.FIXED64 -> TypeName.LONG + Primitive.SFIXED64 -> TypeName.LONG + Primitive.DOUBLE -> TypeName.DOUBLE + Primitive.BOOLEAN -> TypeName.BOOLEAN + Primitive.STRING -> ClassName.get(String::class.java) + Primitive.BYTES -> ArrayTypeName.of(TypeName.BYTE) + is Message -> { + return when (owner) { + is Owner.Package -> ClassName.get(owner.javaName, name) + is Owner.MsgRef -> (owner.message.toTypeName() as ClassName).nestedClass(name) + } } + is ProtoEnum -> { + return when (owner) { + is Owner.Package -> ClassName.get(owner.javaName, name) + is Owner.MsgRef -> (owner.message.toTypeName() as ClassName).nestedClass(name) + } + } + is Unresolved -> throw AssertionError("Impossible!") + } } val ProtoField.typeName: TypeName - get() { - if (!repeated) { - return type.toTypeName() - } - return ParameterizedTypeName.get(ClassName.get(List::class.java), type.boxed) + get() { + if (!repeated) { + return type.toTypeName() } + return ParameterizedTypeName.get(ClassName.get(List::class.java), type.boxed) + } private val ProtobufType.boxed: TypeName - get() { - return when (this) { - Primitive.INT32, Primitive.SINT32, Primitive.FIXED32, Primitive.SFIXED32 -> - ClassName.get("java.lang", "Integer") - Primitive.INT64, Primitive.SINT64, Primitive.FIXED64, Primitive.SFIXED64 -> - ClassName.get("java.lang", "Long") - Primitive.FLOAT -> ClassName.get("java.lang", "Float") - Primitive.DOUBLE -> ClassName.get("java.lang", "Double") - Primitive.BOOLEAN -> ClassName.get("java.lang", "Boolean") - Primitive.STRING -> ClassName.get("java.lang", "String") - Primitive.BYTES -> ArrayTypeName.of(TypeName.BYTE) - else -> toTypeName() - } + get() { + return when (this) { + Primitive.INT32, + Primitive.SINT32, + Primitive.FIXED32, + Primitive.SFIXED32 -> ClassName.get("java.lang", "Integer") + Primitive.INT64, + Primitive.SINT64, + Primitive.FIXED64, + Primitive.SFIXED64 -> ClassName.get("java.lang", "Long") + Primitive.FLOAT -> ClassName.get("java.lang", "Float") + Primitive.DOUBLE -> ClassName.get("java.lang", "Double") + Primitive.BOOLEAN -> ClassName.get("java.lang", "Boolean") + Primitive.STRING -> ClassName.get("java.lang", "String") + Primitive.BYTES -> ArrayTypeName.of(TypeName.BYTE) + else -> toTypeName() } + } val ProtobufType.intEncoding: String? - get() { - return when (this) { - is Primitive.SINT32, Primitive.SINT64 -> - "com.google.firebase.encoders.proto.Protobuf.IntEncoding.SIGNED" - is Primitive.FIXED32, Primitive.FIXED64, Primitive.SFIXED32, Primitive.SFIXED64 -> - "com.google.firebase.encoders.proto.Protobuf.IntEncoding.FIXED" - else -> null - } + get() { + return when (this) { + is Primitive.SINT32, + Primitive.SINT64 -> "com.google.firebase.encoders.proto.Protobuf.IntEncoding.SIGNED" + is Primitive.FIXED32, + Primitive.FIXED64, + Primitive.SFIXED32, + Primitive.SFIXED64 -> "com.google.firebase.encoders.proto.Protobuf.IntEncoding.FIXED" + else -> null } + } val ProtoEnum.defaultValue: String - get() = "$javaName.${values.find { it.value == 0 }?.name}" + get() = "$javaName.${values.find { it.value == 0 }?.name}" class CodeGenerator @Inject constructor(private val config: CodeGenConfig) { - fun generate(messages: Collection): CodeGeneratorResponse { - val gen = Gen(messages, config.vendorPackage) - gen.generate() - - val responseBuilder = CodeGeneratorResponse.newBuilder() - for ((type, typeBuilder) in gen.rootClasses.entries) { - val typeSpec = typeBuilder.build() - val packageName = type.owner.javaName - val out = StringBuilder() - val file = JavaFile.builder(packageName, typeSpec).build() - file.writeTo(out) - val qualifiedName = if (packageName.isEmpty()) typeSpec.name else "$packageName.${typeSpec.name}" - val fileName = "${qualifiedName.replace('.', '/')}.java" - responseBuilder.addFile(CodeGeneratorResponse.File.newBuilder().setContent(out.toString()).setName(fileName)) - } - - return responseBuilder.build() + fun generate(messages: Collection): CodeGeneratorResponse { + val gen = Gen(messages, config.vendorPackage) + gen.generate() + + val responseBuilder = CodeGeneratorResponse.newBuilder() + for ((type, typeBuilder) in gen.rootClasses.entries) { + val typeSpec = typeBuilder.build() + val packageName = type.owner.javaName + val out = StringBuilder() + val file = JavaFile.builder(packageName, typeSpec).build() + file.writeTo(out) + val qualifiedName = + if (packageName.isEmpty()) typeSpec.name else "$packageName.${typeSpec.name}" + val fileName = "${qualifiedName.replace('.', '/')}.java" + responseBuilder.addFile( + CodeGeneratorResponse.File.newBuilder().setContent(out.toString()).setName(fileName) + ) } + + return responseBuilder.build() + } } diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Configuration.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Configuration.kt index 59d394297e9..8eca136c708 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Configuration.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Configuration.kt @@ -18,20 +18,20 @@ import com.google.firebase.encoders.proto.CodeGenConfig import com.google.protobuf.TextFormat object ConfigReader { - fun read(readable: Readable): CodeGenConfig { - val builder = CodeGenConfig.newBuilder() - try { - TextFormat.merge(readable, builder) - } catch (ex: TextFormat.ParseException) { - throw InvalidConfigException("Unable to parse config.", ex) - } - val config = builder.build() - if (config.vendorPackage.isEmpty()) { - throw InvalidConfigException("vendor_package is not set in config.") - } - - return config + fun read(readable: Readable): CodeGenConfig { + val builder = CodeGenConfig.newBuilder() + try { + TextFormat.merge(readable, builder) + } catch (ex: TextFormat.ParseException) { + throw InvalidConfigException("Unable to parse config.", ex) + } + val config = builder.build() + if (config.vendorPackage.isEmpty()) { + throw InvalidConfigException("vendor_package is not set in config.") } + + return config + } } class InvalidConfigException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause) diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Main.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Main.kt index c8df676fa20..32cb9142fc0 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Main.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Main.kt @@ -28,26 +28,22 @@ import java.io.StringWriter import javax.inject.Inject fun driver(input: InputStream, output: OutputStream) { - val request = CodeGeneratorRequest.parseFrom(input) - if (request.parameter.isEmpty()) { - throw InvalidConfigException("Required plugin option is missing. " + - "Please specify the config file path via plugin options.") - } - val cfgFile = File(request.parameter) - if (!cfgFile.exists() || !cfgFile.isFile) { - throw InvalidConfigException("Config file '$cfgFile' does not exist or is a directory.") - } + val request = CodeGeneratorRequest.parseFrom(input) + if (request.parameter.isEmpty()) { + throw InvalidConfigException( + "Required plugin option is missing. " + + "Please specify the config file path via plugin options." + ) + } + val cfgFile = File(request.parameter) + if (!cfgFile.exists() || !cfgFile.isFile) { + throw InvalidConfigException("Config file '$cfgFile' does not exist or is a directory.") + } - val config = cfgFile.reader().use { - ConfigReader.read(it) - } + val config = cfgFile.reader().use { ConfigReader.read(it) } - val component: MainComponent = DaggerMainComponent.builder() - .config(config) - .build() - component.plugin - .run(request.protoFileList) - .writeTo(output) + val component: MainComponent = DaggerMainComponent.builder().config(config).build() + component.plugin.run(request.protoFileList).writeTo(output) } /** @@ -58,33 +54,33 @@ fun driver(input: InputStream, output: OutputStream) { * `stdout`. */ fun main(args: Array) { - runCatching { - driver(System.`in`, System.out) - }.onFailure { - val stringWriter = StringWriter() - it.printStackTrace(PrintWriter(stringWriter)) - CodeGeneratorResponse.newBuilder() - .setError(stringWriter.toString()) - .build() - .writeTo(System.out) + runCatching { driver(System.`in`, System.out) } + .onFailure { + val stringWriter = StringWriter() + it.printStackTrace(PrintWriter(stringWriter)) + CodeGeneratorResponse.newBuilder() + .setError(stringWriter.toString()) + .build() + .writeTo(System.out) } } -class Plugin @Inject constructor(private val parser: DescriptorParser, private val generator: CodeGenerator) { - fun run(protoFiles: List): CodeGeneratorResponse { - return generator.generate(parser.parse(protoFiles)) - } +class Plugin +@Inject +constructor(private val parser: DescriptorParser, private val generator: CodeGenerator) { + fun run(protoFiles: List): CodeGeneratorResponse { + return generator.generate(parser.parse(protoFiles)) + } } @Component(modules = [ParsingModule::class]) interface MainComponent { - val plugin: Plugin + val plugin: Plugin - @Component.Builder - interface Builder { - @BindsInstance - fun config(config: CodeGenConfig): Builder + @Component.Builder + interface Builder { + @BindsInstance fun config(config: CodeGenConfig): Builder - fun build(): MainComponent - } + fun build(): MainComponent + } } diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Parsing.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Parsing.kt index ba4f00899ab..aacfc991338 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Parsing.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Parsing.kt @@ -26,11 +26,9 @@ import dagger.Binds import dagger.Module import javax.inject.Inject -/** - * Transforms the protobuf message descriptors into a message graph for later use in codegen. - */ +/** Transforms the protobuf message descriptors into a message graph for later use in codegen. */ interface DescriptorParser { - fun parse(protoFiles: List): Collection + fun parse(protoFiles: List): Collection } /** @@ -43,123 +41,134 @@ interface DescriptorParser { * extensions are indistinguishable from message's own fields. */ class DefaultParser @Inject constructor(private val config: CodeGenConfig) : DescriptorParser { - override fun parse(files: List): List { - val parsedTypes = files.asSequence().flatMap { file -> - val javaPackage = "${config.vendorPackage}.${file.`package`}" - - val parent = Owner.Package(file.`package`, javaPackage, file.name) - parseEnums(parent, file.enumTypeList).plus(parseMessages(parent, file.messageTypeList)) - }.toList() + override fun parse(files: List): List { + val parsedTypes = + files + .asSequence() + .flatMap { file -> + val javaPackage = "${config.vendorPackage}.${file.`package`}" + + val parent = Owner.Package(file.`package`, javaPackage, file.name) + parseEnums(parent, file.enumTypeList).plus(parseMessages(parent, file.messageTypeList)) + } + .toList() - val extensions = discoverExtensions(files) + val extensions = discoverExtensions(files) - for (type in parsedTypes) { - val msgExtensions = extensions[type.protobufFullName] - if (type !is Message || msgExtensions == null) { - continue - } - type.addFields(msgExtensions) - } - resolveReferences(parsedTypes) - return parsedTypes.filter { - config.includeList.contains(it.protobufFullName) - } + for (type in parsedTypes) { + val msgExtensions = extensions[type.protobufFullName] + if (type !is Message || msgExtensions == null) { + continue + } + type.addFields(msgExtensions) } - - private fun resolveReferences(types: Collection) { - val messageIndex = types.asSequence() - .map { it.protobufFullName to it } - .toMap() - for (userDefined in types) { - if (userDefined !is Message) { - continue - } - for (field in userDefined.fields) { - (field.type as? Unresolved)?.run { - field.type = messageIndex[this.protobufName] - ?: throw IllegalArgumentException("Unresolved reference to $protobufName in ${userDefined.protobufFullName}.") - } - } + resolveReferences(parsedTypes) + return parsedTypes.filter { config.includeList.contains(it.protobufFullName) } + } + + private fun resolveReferences(types: Collection) { + val messageIndex = types.asSequence().map { it.protobufFullName to it }.toMap() + for (userDefined in types) { + if (userDefined !is Message) { + continue + } + for (field in userDefined.fields) { + (field.type as? Unresolved)?.run { + field.type = + messageIndex[this.protobufName] + ?: throw IllegalArgumentException( + "Unresolved reference to $protobufName in ${userDefined.protobufFullName}." + ) } + } } - - private fun discoverExtensions( - files: List - ): ImmutableMultimap { - - val extensions: ImmutableListMultimap.Builder = - ImmutableListMultimap.builder() - for (file in files) { - for (field in file.extensionList) { - extensions.put( - field.extendee.trimStart('.'), - ProtoField( - field.name, - field.determineType(), - field.number, - field.label == FieldDescriptorProto.Label.LABEL_REPEATED)) - } - } - return extensions.build() + } + + private fun discoverExtensions( + files: List + ): ImmutableMultimap { + + val extensions: ImmutableListMultimap.Builder = + ImmutableListMultimap.builder() + for (file in files) { + for (field in file.extensionList) { + extensions.put( + field.extendee.trimStart('.'), + ProtoField( + field.name, + field.determineType(), + field.number, + field.label == FieldDescriptorProto.Label.LABEL_REPEATED + ) + ) + } } - - private fun parseEnums(parent: Owner, enums: List): Sequence { - return enums.asSequence().map { enum -> - ProtoEnum(parent, enum.name, enum.valueList.map { - ProtoEnum.Value(it.name, it.number) - }) - } + return extensions.build() + } + + private fun parseEnums( + parent: Owner, + enums: List + ): Sequence { + return enums.asSequence().map { enum -> + ProtoEnum(parent, enum.name, enum.valueList.map { ProtoEnum.Value(it.name, it.number) }) } + } + + private fun parseMessages( + parent: Owner, + messages: List + ): Sequence { + return messages.asSequence().flatMap { msg -> + val m = + Message( + owner = parent, + name = msg.name, + fields = + msg.fieldList.map { + ProtoField( + name = it.name, + type = it.determineType(), + number = it.number, + repeated = it.label == FieldDescriptorProto.Label.LABEL_REPEATED + ) + } + ) + val newParent = Owner.MsgRef(m) - private fun parseMessages(parent: Owner, messages: List): Sequence { - return messages.asSequence().flatMap { msg -> - val m = Message( - owner = parent, - name = msg.name, - fields = msg.fieldList.map { - ProtoField( - name = it.name, - type = it.determineType(), - number = it.number, - repeated = it.label == FieldDescriptorProto.Label.LABEL_REPEATED - ) - }) - val newParent = Owner.MsgRef(m) - - sequenceOf(m) - .plus(parseEnums(newParent, msg.enumTypeList)) - .plus(parseMessages(newParent, msg.nestedTypeList)) - } + sequenceOf(m) + .plus(parseEnums(newParent, msg.enumTypeList)) + .plus(parseMessages(newParent, msg.nestedTypeList)) } + } } private fun FieldDescriptorProto.determineType(): ProtobufType { - if (typeName != "") { - return Unresolved(typeName.trimStart('.')) - } - - return when (type) { - FieldDescriptorProto.Type.TYPE_INT32 -> Primitive.INT32 - FieldDescriptorProto.Type.TYPE_DOUBLE -> Primitive.DOUBLE - FieldDescriptorProto.Type.TYPE_FLOAT -> Primitive.FLOAT - FieldDescriptorProto.Type.TYPE_INT64 -> Primitive.INT64 - FieldDescriptorProto.Type.TYPE_UINT64 -> Primitive.INT64 - FieldDescriptorProto.Type.TYPE_FIXED64 -> Primitive.FIXED64 - FieldDescriptorProto.Type.TYPE_FIXED32 -> Primitive.FIXED32 - FieldDescriptorProto.Type.TYPE_BOOL -> Primitive.BOOLEAN - FieldDescriptorProto.Type.TYPE_STRING -> Primitive.STRING - FieldDescriptorProto.Type.TYPE_BYTES -> Primitive.BYTES - FieldDescriptorProto.Type.TYPE_UINT32 -> Primitive.INT32 - FieldDescriptorProto.Type.TYPE_SFIXED32 -> Primitive.SFIXED32 - FieldDescriptorProto.Type.TYPE_SFIXED64 -> Primitive.SFIXED64 - FieldDescriptorProto.Type.TYPE_SINT32 -> Primitive.SINT32 - FieldDescriptorProto.Type.TYPE_SINT64 -> Primitive.SINT64 - else -> throw IllegalArgumentException("$type is not supported") - } + if (typeName != "") { + return Unresolved(typeName.trimStart('.')) + } + + return when (type) { + FieldDescriptorProto.Type.TYPE_INT32 -> Primitive.INT32 + FieldDescriptorProto.Type.TYPE_DOUBLE -> Primitive.DOUBLE + FieldDescriptorProto.Type.TYPE_FLOAT -> Primitive.FLOAT + FieldDescriptorProto.Type.TYPE_INT64 -> Primitive.INT64 + FieldDescriptorProto.Type.TYPE_UINT64 -> Primitive.INT64 + FieldDescriptorProto.Type.TYPE_FIXED64 -> Primitive.FIXED64 + FieldDescriptorProto.Type.TYPE_FIXED32 -> Primitive.FIXED32 + FieldDescriptorProto.Type.TYPE_BOOL -> Primitive.BOOLEAN + FieldDescriptorProto.Type.TYPE_STRING -> Primitive.STRING + FieldDescriptorProto.Type.TYPE_BYTES -> Primitive.BYTES + FieldDescriptorProto.Type.TYPE_UINT32 -> Primitive.INT32 + FieldDescriptorProto.Type.TYPE_SFIXED32 -> Primitive.SFIXED32 + FieldDescriptorProto.Type.TYPE_SFIXED64 -> Primitive.SFIXED64 + FieldDescriptorProto.Type.TYPE_SINT32 -> Primitive.SINT32 + FieldDescriptorProto.Type.TYPE_SINT64 -> Primitive.SINT64 + else -> throw IllegalArgumentException("$type is not supported") + } } @Module abstract class ParsingModule { - @Binds - abstract fun bindParser(parser: DefaultParser): DescriptorParser + @Binds abstract fun bindParser(parser: DefaultParser): DescriptorParser } diff --git a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt index f87b12504d8..6d8f1af7fc0 100644 --- a/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt +++ b/encoders/protoc-gen-firebase-encoders/src/main/kotlin/com/google/firebase/encoders/proto/codegen/Types.kt @@ -16,7 +16,7 @@ package com.google.firebase.encoders.proto.codegen /** Represents a protocol buffer type. */ sealed class ProtobufType { - abstract val javaName: String + abstract val javaName: String } /** @@ -28,23 +28,23 @@ sealed class ProtobufType { * @property defaultValue Default literal value of a primitive type according to the proto3 spec. */ sealed class Primitive(override val javaName: String, val defaultValue: String) : ProtobufType() { - override fun toString(): String = this::class.java.simpleName - - object INT32 : Primitive("int", "0") - object SINT32 : Primitive("int", "0") - object FIXED32 : Primitive("int", "0") - object SFIXED32 : Primitive("int", "0") - object FLOAT : Primitive("float", "0") - - object INT64 : Primitive("long", "0") - object SINT64 : Primitive("long", "0") - object FIXED64 : Primitive("long", "0") - object SFIXED64 : Primitive("long", "0") - object DOUBLE : Primitive("double", "0") - - object BOOLEAN : Primitive("boolean", "false") - object STRING : Primitive("String", "\"\"") - object BYTES : Primitive("byte[]", "new byte[0]") + override fun toString(): String = this::class.java.simpleName + + object INT32 : Primitive("int", "0") + object SINT32 : Primitive("int", "0") + object FIXED32 : Primitive("int", "0") + object SFIXED32 : Primitive("int", "0") + object FLOAT : Primitive("float", "0") + + object INT64 : Primitive("long", "0") + object SINT64 : Primitive("long", "0") + object FIXED64 : Primitive("long", "0") + object SFIXED64 : Primitive("long", "0") + object DOUBLE : Primitive("double", "0") + + object BOOLEAN : Primitive("boolean", "false") + object STRING : Primitive("String", "\"\"") + object BYTES : Primitive("byte[]", "new byte[0]") } /** @@ -53,49 +53,52 @@ sealed class Primitive(override val javaName: String, val defaultValue: String) * Can be one of [Message] or [ProtoEnum]. */ sealed class UserDefined : ProtobufType() { - /** A fully qualified protobuf message name, i.e. `com.example.Outer.Inner`. */ - abstract val protobufFullName: String - - /** Specifies the scope that this type is defined in, i.e. a [Owner.Package] or a parent [Message]. */ - abstract val owner: Owner - - /** Unqualified name of this type */ - abstract val name: String - - /** A fully qualified java name of this type, i.e. `com.example.Outer$Inner`. */ - override val javaName: String - get() = "${owner.javaName}${owner.scopeSeparator}$name" - - /** Represents a protobuf `message` type. */ - class Message( - override val owner: Owner, - override val name: String, - fields: List - ) : UserDefined() { - private var mutableFields: List = fields - override val protobufFullName: String - get() = "${owner.protobufFullName}.$name" - - override fun toString(): String { - return "Message(owner=$owner,name=$name,fields=$fields)" - } - - val fields: List - get() = mutableFields - - fun addFields(fields: Iterable) { - mutableFields = mutableFields + fields - } + /** A fully qualified protobuf message name, i.e. `com.example.Outer.Inner`. */ + abstract val protobufFullName: String + + /** + * Specifies the scope that this type is defined in, i.e. a [Owner.Package] or a parent [Message]. + */ + abstract val owner: Owner + + /** Unqualified name of this type */ + abstract val name: String + + /** A fully qualified java name of this type, i.e. `com.example.Outer$Inner`. */ + override val javaName: String + get() = "${owner.javaName}${owner.scopeSeparator}$name" + + /** Represents a protobuf `message` type. */ + class Message(override val owner: Owner, override val name: String, fields: List) : + UserDefined() { + private var mutableFields: List = fields + override val protobufFullName: String + get() = "${owner.protobufFullName}.$name" + + override fun toString(): String { + return "Message(owner=$owner,name=$name,fields=$fields)" } - /** Represents a protobuf `enum` type. */ - data class ProtoEnum(override val owner: Owner, override val name: String, val values: List) : UserDefined() { - override val protobufFullName: String - get() = "${owner.protobufFullName}.$name" + val fields: List + get() = mutableFields - /** Represents possible enum values including name and field number. */ - data class Value(val name: String, val value: Int) + fun addFields(fields: Iterable) { + mutableFields = mutableFields + fields } + } + + /** Represents a protobuf `enum` type. */ + data class ProtoEnum( + override val owner: Owner, + override val name: String, + val values: List + ) : UserDefined() { + override val protobufFullName: String + get() = "${owner.protobufFullName}.$name" + + /** Represents possible enum values including name and field number. */ + data class Value(val name: String, val value: Int) + } } /** @@ -106,18 +109,18 @@ sealed class UserDefined : ProtobufType() { * possible, when a message `A` has a field of type `A` either directly or transitively. * * To address that all non-primitive fields are initially set to [Unresolved], and once all messages - * are parsed, all unresolved references are replaced with their respective - * [protobuf types][protobufName]. + * are parsed, all unresolved references are replaced with their respective [protobuf types] + * [protobufName]. * * @property protobufName Fully-qualified protobuf name of the message/enum. */ data class Unresolved(val protobufName: String) : ProtobufType() { - override val javaName: String - get() = - throw UnsupportedOperationException( - "Unresolved types don't have a javaName, they are intended to be resolved " + - "after parsing is complete." - ) + override val javaName: String + get() = + throw UnsupportedOperationException( + "Unresolved types don't have a javaName, they are intended to be resolved " + + "after parsing is complete." + ) } /** @@ -128,30 +131,31 @@ data class Unresolved(val protobufName: String) : ProtobufType() { * [Owner.Package] or [UserDefined.Message] (represented by [Owner.MsgRef]) respectively. */ sealed class Owner(val scopeSeparator: Char) { - abstract val protobufFullName: String - abstract val fileName: String - abstract val javaName: String + abstract val protobufFullName: String + abstract val fileName: String + abstract val javaName: String - /** Represents a package that a protobuf type belongs to. */ - data class Package(val name: String, val javaPackage: String, override val fileName: String) : Owner('.') { - override val protobufFullName: String - get() = name + /** Represents a package that a protobuf type belongs to. */ + data class Package(val name: String, val javaPackage: String, override val fileName: String) : + Owner('.') { + override val protobufFullName: String + get() = name - override val javaName: String - get() = javaPackage - } - - /** Represents a message that contains nested protobuf types. */ - data class MsgRef(val message: UserDefined.Message) : Owner('$') { - override val protobufFullName: String - get() = message.protobufFullName - override val fileName: String - get() = message.owner.fileName - override val javaName: String - get() = message.javaName + override val javaName: String + get() = javaPackage + } + + /** Represents a message that contains nested protobuf types. */ + data class MsgRef(val message: UserDefined.Message) : Owner('$') { + override val protobufFullName: String + get() = message.protobufFullName + override val fileName: String + get() = message.owner.fileName + override val javaName: String + get() = message.javaName - override fun toString(): String = "MsgRef(name=${message.protobufFullName})" - } + override fun toString(): String = "MsgRef(name=${message.protobufFullName})" + } } private val SNAKE_CASE_REGEX = "_[a-zA-Z]".toRegex() @@ -161,11 +165,19 @@ private val SNAKE_CASE_REGEX = "_[a-zA-Z]".toRegex() * * @property name name of the field as defined in the proto file, usually camel_cased. * @property type this property is mutable because it's not always possible specify the type + * ``` * upfront. See [Unresolved] for details. - * */ -data class ProtoField(val name: String, var type: ProtobufType, val number: Int, val repeated: Boolean = false) { - /** Custom toString() needed to avoid stackoverflow if case of message reference cycles. */ - override fun toString(): String = "ProtoField(\"$name\":$number, ${ + * ``` + */ +data class ProtoField( + val name: String, + var type: ProtobufType, + val number: Int, + val repeated: Boolean = false +) { + /** Custom toString() needed to avoid stackoverflow if case of message reference cycles. */ + override fun toString(): String = + "ProtoField(\"$name\":$number, ${ type.let { when (it) { is UserDefined.Message -> it.protobufFullName @@ -174,14 +186,11 @@ data class ProtoField(val name: String, var type: ProtobufType, val number: Int, } })" - val lowerCamelCaseName: String - get() { - return SNAKE_CASE_REGEX.replace(name) { - it.value.replace("_", "") - .toUpperCase() - } - } + val lowerCamelCaseName: String + get() { + return SNAKE_CASE_REGEX.replace(name) { it.value.replace("_", "").toUpperCase() } + } - val camelCaseName: String - get() = lowerCamelCaseName.capitalize() + val camelCaseName: String + get() = lowerCamelCaseName.capitalize() } diff --git a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ConfigTests.kt b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ConfigTests.kt index 7c87cbf83b5..2eaf3fce33b 100644 --- a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ConfigTests.kt +++ b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ConfigTests.kt @@ -24,27 +24,29 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class ConfigTests { - @Test - fun `read() with valid txtpb file should succeed`() { - val vendorPackage = "com.example" - val myProto = "com.example.proto.MyProto" - val config = ConfigReader.read(""" + @Test + fun `read() with valid txtpb file should succeed`() { + val vendorPackage = "com.example" + val myProto = "com.example.proto.MyProto" + val config = + ConfigReader.read( + """ vendor_package: "$vendorPackage" include: "$myProto" - """.trimIndent()) + """ + .trimIndent() + ) - assertThat(config).isEqualTo(CodeGenConfig.newBuilder() - .setVendorPackage(vendorPackage) - .addInclude(myProto) - .build()) - } + assertThat(config) + .isEqualTo( + CodeGenConfig.newBuilder().setVendorPackage(vendorPackage).addInclude(myProto).build() + ) + } - @Test - fun `read() with invalid file should fail`() { - assertThrows(InvalidConfigException::class.java) { - ConfigReader.read("invalid") - } - } + @Test + fun `read() with invalid file should fail`() { + assertThrows(InvalidConfigException::class.java) { ConfigReader.read("invalid") } + } } private fun ConfigReader.read(value: String): CodeGenConfig = read(CharBuffer.wrap(value)) diff --git a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/DriverTests.kt b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/DriverTests.kt index 6e1093bd872..5c84e4bdbed 100644 --- a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/DriverTests.kt +++ b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/DriverTests.kt @@ -25,70 +25,60 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class DriverTests { - object DevNullOutputStream : OutputStream() { - override fun write(b: Int) {} - } + object DevNullOutputStream : OutputStream() { + override fun write(b: Int) {} + } - @Test - fun `driver should throw if config file is not specified`() { - val thrown = assertThrows(InvalidConfigException::class.java) { - driver(CodeGeneratorRequest.getDefaultInstance().asInput(), DevNullOutputStream) - } - assertThat(thrown.message).contains("Required plugin option is missing") - } - @Test - fun `driver should throw if config file does not exist`() { - val requestInput = CodeGeneratorRequest.newBuilder() - .setParameter("does_not_exist.txtpb") - .build() - .asInput() - val thrown = assertThrows(InvalidConfigException::class.java) { - driver(requestInput, DevNullOutputStream) - } - assertThat(thrown.message).contains("does not exist") - } + @Test + fun `driver should throw if config file is not specified`() { + val thrown = + assertThrows(InvalidConfigException::class.java) { + driver(CodeGeneratorRequest.getDefaultInstance().asInput(), DevNullOutputStream) + } + assertThat(thrown.message).contains("Required plugin option is missing") + } + @Test + fun `driver should throw if config file does not exist`() { + val requestInput = + CodeGeneratorRequest.newBuilder().setParameter("does_not_exist.txtpb").build().asInput() + val thrown = + assertThrows(InvalidConfigException::class.java) { driver(requestInput, DevNullOutputStream) } + assertThat(thrown.message).contains("does not exist") + } - @Test - fun `driver should throw if config file has syntax error`() { - val cfg = File.createTempFile("invalid_cfg", null) - cfg.writeText("invalid") - val requestInput = CodeGeneratorRequest.newBuilder() - .setParameter(cfg.absolutePath) - .build() - .asInput() + @Test + fun `driver should throw if config file has syntax error`() { + val cfg = File.createTempFile("invalid_cfg", null) + cfg.writeText("invalid") + val requestInput = + CodeGeneratorRequest.newBuilder().setParameter(cfg.absolutePath).build().asInput() - val thrown = assertThrows(InvalidConfigException::class.java) { - driver(requestInput, DevNullOutputStream) - } - assertThat(thrown.cause).isInstanceOf(TextFormat.ParseException::class.java) - } + val thrown = + assertThrows(InvalidConfigException::class.java) { driver(requestInput, DevNullOutputStream) } + assertThat(thrown.cause).isInstanceOf(TextFormat.ParseException::class.java) + } - @Test - fun `driver should throw if vendor_package is empty`() { - val cfg = File.createTempFile("invalid_cfg", null) - cfg.writeText("include: \"\"") - val requestInput = CodeGeneratorRequest.newBuilder() - .setParameter(cfg.absolutePath) - .build() - .asInput() + @Test + fun `driver should throw if vendor_package is empty`() { + val cfg = File.createTempFile("invalid_cfg", null) + cfg.writeText("include: \"\"") + val requestInput = + CodeGeneratorRequest.newBuilder().setParameter(cfg.absolutePath).build().asInput() - val thrown = assertThrows(InvalidConfigException::class.java) { - driver(requestInput, DevNullOutputStream) - } - assertThat(thrown.message).contains("vendor_package is not set") - } + val thrown = + assertThrows(InvalidConfigException::class.java) { driver(requestInput, DevNullOutputStream) } + assertThat(thrown.message).contains("vendor_package is not set") + } - @Test - fun `driver should succeed if config is valid`() { - val cfg = File.createTempFile("valid_cfg", null) - cfg.writeText("vendor_package: \"com.example\"") - val requestInput = CodeGeneratorRequest.newBuilder() - .setParameter(cfg.absolutePath) - .build() - .asInput() + @Test + fun `driver should succeed if config is valid`() { + val cfg = File.createTempFile("valid_cfg", null) + cfg.writeText("vendor_package: \"com.example\"") + val requestInput = + CodeGeneratorRequest.newBuilder().setParameter(cfg.absolutePath).build().asInput() - driver(requestInput, DevNullOutputStream) - } + driver(requestInput, DevNullOutputStream) + } } private fun CodeGeneratorRequest.asInput() = toByteString().newInput() diff --git a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ParsingTests.kt b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ParsingTests.kt index d2b101bcc39..779ffcf7735 100644 --- a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ParsingTests.kt +++ b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/ParsingTests.kt @@ -31,139 +31,152 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class ParsingTests { - @Test - fun `parse with simple message, enum and import should succeed`() { - val result = parse( - SimpleProto.getDescriptor().file, - Timestamp.getDescriptor().file, - include = listOf(SimpleProto.getDefaultInstance())) - - val msg = result.messageAt(0) - assertThat(msg.protobufFullName).isEqualTo("com.google.firebase.testing.SimpleProto") - assertThat(msg.javaName).isEqualTo("com.example.com.google.firebase.testing.SimpleProto") - - assertThat(msg.fields).containsAtLeast( - ProtoField(name = "value", number = 1, type = Primitive.INT32), - ProtoField(name = "str", number = 2, type = Primitive.STRING, repeated = true) - ) - val timestamp = msg.fields.withName("time") - assertThat(timestamp.number).isEqualTo(3) - assertThat(timestamp.repeated).isEqualTo(false) - - val tsMsg = timestamp.messageType - assertThat(tsMsg.javaName).isEqualTo("com.example.google.protobuf.Timestamp") - assertThat(tsMsg.protobufFullName).isEqualTo("google.protobuf.Timestamp") - assertThat(tsMsg.fields).containsExactly( - ProtoField(name = "seconds", number = 1, type = Primitive.INT64), - ProtoField(name = "nanos", number = 2, type = Primitive.INT32) - ) - - val enum = msg.fields.withName("my_enum").type as? ProtoEnum - ?: throw AssertionError("my_enum is not an enum") - - assertThat(enum.javaName).isEqualTo("com.example.com.google.firebase.testing.SimpleProto\$MyEnum") - assertThat(enum.values).containsExactly( - ProtoEnum.Value("DEFAULT", 0), - ProtoEnum.Value("EXAMPLE", 1) - ) - } - - @Test - fun `parse all primitive types should succeed`() { - - val result = parse( - Types.getDescriptor().file, - include = listOf(Types.getDefaultInstance())) - - val msg = result.messageAt(0) - assertThat(msg.protobufFullName).isEqualTo("com.google.firebase.testing.Types") - assertThat(msg.javaName).isEqualTo("com.example.com.google.firebase.testing.Types") - - val types32 = msg.messageFieldNamed("types32") - - assertThat(types32.fields).containsExactly( - ProtoField(name = "i", number = 1, type = Primitive.INT32), - ProtoField(name = "si", number = 2, type = Primitive.SINT32), - ProtoField(name = "f", number = 3, type = Primitive.FIXED32), - ProtoField(name = "sf", number = 4, type = Primitive.SFIXED32), - ProtoField(name = "fl", number = 5, type = Primitive.FLOAT), - ProtoField(name = "ui", number = 6, type = Primitive.INT32) - ) - - val types64 = msg.messageFieldNamed("types64") - - assertThat(types64.fields).containsExactly( - ProtoField(name = "i", number = 1, type = Primitive.INT64), - ProtoField(name = "si", number = 2, type = Primitive.SINT64), - ProtoField(name = "f", number = 3, type = Primitive.FIXED64), - ProtoField(name = "sf", number = 4, type = Primitive.SFIXED64), - ProtoField(name = "db", number = 5, type = Primitive.DOUBLE), - ProtoField(name = "ui", number = 6, type = Primitive.INT64) - ) - - val other = msg.messageFieldNamed("other") - - assertThat(other.fields).containsExactly( - ProtoField(name = "boolean", number = 1, type = Primitive.BOOLEAN), - ProtoField(name = "str", number = 2, type = Primitive.STRING), - ProtoField(name = "bts", number = 3, type = Primitive.BYTES) - ) - } - - @Test - fun `parse with self-referencing message should succeed`() { - val result = parse( - LinkedListProto.getDescriptor().file, - include = listOf(LinkedListProto.getDefaultInstance())) - - val msg = result.messageAt(0) - assertThat(msg.name).isEqualTo("LinkedListProto") - assertThat(msg.fields).containsExactly( - ProtoField(name = "value", number = 1, type = Primitive.INT32), - ProtoField(name = "self", number = 2, type = msg)) - } - - @Test - fun `parse with extensions should add fields to extended message`() { - val result = parse( - Extendable.getDescriptor().file, - include = listOf(Extendable.getDefaultInstance())) - - val msg = result.messageAt(0) - assertThat(msg.fields).hasSize(2) - - assertThat(msg.fields.withName("extended")).isEqualTo( - ProtoField(name = "extended", number = 101, type = Primitive.STRING)) - - val nested = msg.messageFieldNamed("nested") - val ext = nested.messageFieldNamed("ext") - assertThat(ext).isSameInstanceAs(msg) - } + @Test + fun `parse with simple message, enum and import should succeed`() { + val result = + parse( + SimpleProto.getDescriptor().file, + Timestamp.getDescriptor().file, + include = listOf(SimpleProto.getDefaultInstance()) + ) + + val msg = result.messageAt(0) + assertThat(msg.protobufFullName).isEqualTo("com.google.firebase.testing.SimpleProto") + assertThat(msg.javaName).isEqualTo("com.example.com.google.firebase.testing.SimpleProto") + + assertThat(msg.fields) + .containsAtLeast( + ProtoField(name = "value", number = 1, type = Primitive.INT32), + ProtoField(name = "str", number = 2, type = Primitive.STRING, repeated = true) + ) + val timestamp = msg.fields.withName("time") + assertThat(timestamp.number).isEqualTo(3) + assertThat(timestamp.repeated).isEqualTo(false) + + val tsMsg = timestamp.messageType + assertThat(tsMsg.javaName).isEqualTo("com.example.google.protobuf.Timestamp") + assertThat(tsMsg.protobufFullName).isEqualTo("google.protobuf.Timestamp") + assertThat(tsMsg.fields) + .containsExactly( + ProtoField(name = "seconds", number = 1, type = Primitive.INT64), + ProtoField(name = "nanos", number = 2, type = Primitive.INT32) + ) + + val enum = + msg.fields.withName("my_enum").type as? ProtoEnum + ?: throw AssertionError("my_enum is not an enum") + + assertThat(enum.javaName) + .isEqualTo("com.example.com.google.firebase.testing.SimpleProto\$MyEnum") + assertThat(enum.values) + .containsExactly(ProtoEnum.Value("DEFAULT", 0), ProtoEnum.Value("EXAMPLE", 1)) + } + + @Test + fun `parse all primitive types should succeed`() { + + val result = parse(Types.getDescriptor().file, include = listOf(Types.getDefaultInstance())) + + val msg = result.messageAt(0) + assertThat(msg.protobufFullName).isEqualTo("com.google.firebase.testing.Types") + assertThat(msg.javaName).isEqualTo("com.example.com.google.firebase.testing.Types") + + val types32 = msg.messageFieldNamed("types32") + + assertThat(types32.fields) + .containsExactly( + ProtoField(name = "i", number = 1, type = Primitive.INT32), + ProtoField(name = "si", number = 2, type = Primitive.SINT32), + ProtoField(name = "f", number = 3, type = Primitive.FIXED32), + ProtoField(name = "sf", number = 4, type = Primitive.SFIXED32), + ProtoField(name = "fl", number = 5, type = Primitive.FLOAT), + ProtoField(name = "ui", number = 6, type = Primitive.INT32) + ) + + val types64 = msg.messageFieldNamed("types64") + + assertThat(types64.fields) + .containsExactly( + ProtoField(name = "i", number = 1, type = Primitive.INT64), + ProtoField(name = "si", number = 2, type = Primitive.SINT64), + ProtoField(name = "f", number = 3, type = Primitive.FIXED64), + ProtoField(name = "sf", number = 4, type = Primitive.SFIXED64), + ProtoField(name = "db", number = 5, type = Primitive.DOUBLE), + ProtoField(name = "ui", number = 6, type = Primitive.INT64) + ) + + val other = msg.messageFieldNamed("other") + + assertThat(other.fields) + .containsExactly( + ProtoField(name = "boolean", number = 1, type = Primitive.BOOLEAN), + ProtoField(name = "str", number = 2, type = Primitive.STRING), + ProtoField(name = "bts", number = 3, type = Primitive.BYTES) + ) + } + + @Test + fun `parse with self-referencing message should succeed`() { + val result = + parse( + LinkedListProto.getDescriptor().file, + include = listOf(LinkedListProto.getDefaultInstance()) + ) + + val msg = result.messageAt(0) + assertThat(msg.name).isEqualTo("LinkedListProto") + assertThat(msg.fields) + .containsExactly( + ProtoField(name = "value", number = 1, type = Primitive.INT32), + ProtoField(name = "self", number = 2, type = msg) + ) + } + + @Test + fun `parse with extensions should add fields to extended message`() { + val result = + parse(Extendable.getDescriptor().file, include = listOf(Extendable.getDefaultInstance())) + + val msg = result.messageAt(0) + assertThat(msg.fields).hasSize(2) + + assertThat(msg.fields.withName("extended")) + .isEqualTo(ProtoField(name = "extended", number = 101, type = Primitive.STRING)) + + val nested = msg.messageFieldNamed("nested") + val ext = nested.messageFieldNamed("ext") + assertThat(ext).isSameInstanceAs(msg) + } } -private fun parse(vararg files: FileDescriptor, include: List = listOf()): List { - return DefaultParser(CodeGenConfig.newBuilder() - .setVendorPackage("com.example") - .addAllInclude(include.map { it.descriptorForType.fullName }) - .build()).parse(files.map { it.toProto() }) +private fun parse( + vararg files: FileDescriptor, + include: List = listOf() +): List { + return DefaultParser( + CodeGenConfig.newBuilder() + .setVendorPackage("com.example") + .addAllInclude(include.map { it.descriptorForType.fullName }) + .build() + ) + .parse(files.map { it.toProto() }) } private fun List.messageAt(index: Int): Message { - assertThat(this).hasSize(index + 1) - assertThat(this[index]).isInstanceOf(Message::class.java) - return this[index] as Message + assertThat(this).hasSize(index + 1) + assertThat(this[index]).isInstanceOf(Message::class.java) + return this[index] as Message } private fun List.withName(name: String): ProtoField { - return this.find { it.name == name } ?: throw AssertionError( - "Did not find expected $name field in the message" - ) + return this.find { it.name == name } + ?: throw AssertionError("Did not find expected $name field in the message") } private val ProtoField.messageType: Message - get() = type as? Message ?: throw AssertionError("Field $name is not a message.") + get() = type as? Message ?: throw AssertionError("Field $name is not a message.") private fun Message.messageFieldNamed(name: String): Message { - return this.fields.asSequence().find { it.name == name }?.messageType - ?: throw AssertionError("Did not find expected $name field in the message") + return this.fields.asSequence().find { it.name == name }?.messageType + ?: throw AssertionError("Did not find expected $name field in the message") } diff --git a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/TypeTests.kt b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/TypeTests.kt index 8f89fe0afc4..8c3a29fd38a 100644 --- a/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/TypeTests.kt +++ b/encoders/protoc-gen-firebase-encoders/src/test/kotlin/com/google/firebase/encoders/proto/codegen/TypeTests.kt @@ -21,74 +21,59 @@ import org.junit.runners.JUnit4 @RunWith(JUnit4::class) class TypeTests { - @Test - fun `proto and java qualified names work as expected`() { - val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") - val helloMsg = UserDefined.Message(owner = package1, name = "Hello", fields = listOf()) - val worldMsg = UserDefined.Message(Owner.MsgRef(helloMsg), "World", fields = listOf()) + @Test + fun `proto and java qualified names work as expected`() { + val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") + val helloMsg = UserDefined.Message(owner = package1, name = "Hello", fields = listOf()) + val worldMsg = UserDefined.Message(Owner.MsgRef(helloMsg), "World", fields = listOf()) - assertThat(helloMsg.protobufFullName).isEqualTo("com.example.Hello") - assertThat(helloMsg.javaName).isEqualTo("com.example.proto.Hello") + assertThat(helloMsg.protobufFullName).isEqualTo("com.example.Hello") + assertThat(helloMsg.javaName).isEqualTo("com.example.proto.Hello") - assertThat(worldMsg.protobufFullName).isEqualTo("com.example.Hello.World") - assertThat(worldMsg.javaName).isEqualTo("com.example.proto.Hello\$World") - } + assertThat(worldMsg.protobufFullName).isEqualTo("com.example.Hello.World") + assertThat(worldMsg.javaName).isEqualTo("com.example.proto.Hello\$World") + } - @Test - fun `ProtoField#toString() method should not cause stack overflow`() { - /* - syntax = "proto3"; - package com.example; - option java_package = "com.example.proto"; + @Test + fun `ProtoField#toString() method should not cause stack overflow`() { + /* + syntax = "proto3"; + package com.example; + option java_package = "com.example.proto"; - message Hello { - message World { - Hello hello = 1; - int64 my_long = 2; - } - World world = 1; - repeated int32 my_int = 2; + message Hello { + message World { + Hello hello = 1; + int64 my_long = 2; } - */ - val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") - val worldField = ProtoField( - name = "world", - type = Unresolved("com.example.Hello.World"), - number = 1 - ) - val helloField = ProtoField( - name = "hello", - type = Unresolved("com.example.Hello"), - number = 1 - ) - val helloMsg = UserDefined.Message( - owner = package1, - name = "Hello", - fields = listOf( - worldField, - ProtoField( - name = "my_int", - type = Primitive.INT32, - number = 2, - repeated = true - ) - ) - ) - val worldMsg = UserDefined.Message( - owner = Owner.MsgRef(helloMsg), - name = "World", - fields = listOf( - helloField, - ProtoField( - name = "my_long", - type = Primitive.INT64, - number = 2 - ) - ) - ) - worldField.type = worldMsg - helloField.type = helloMsg - - assertThat(helloMsg.fields[0].toString()).contains("com.example.Hello.World") + World world = 1; + repeated int32 my_int = 2; } + */ + val package1 = Owner.Package("com.example", "com.example.proto", "my_proto.proto") + val worldField = + ProtoField(name = "world", type = Unresolved("com.example.Hello.World"), number = 1) + val helloField = ProtoField(name = "hello", type = Unresolved("com.example.Hello"), number = 1) + val helloMsg = + UserDefined.Message( + owner = package1, + name = "Hello", + fields = + listOf( + worldField, + ProtoField(name = "my_int", type = Primitive.INT32, number = 2, repeated = true) + ) + ) + val worldMsg = + UserDefined.Message( + owner = Owner.MsgRef(helloMsg), + name = "World", + fields = + listOf(helloField, ProtoField(name = "my_long", type = Primitive.INT64, number = 2)) + ) + worldField.type = worldMsg + helloField.type = helloMsg + + assertThat(helloMsg.fields[0].toString()).contains("com.example.Hello.World") + } } diff --git a/encoders/protoc-gen-firebase-encoders/tests/tests.gradle b/encoders/protoc-gen-firebase-encoders/tests/tests.gradle index 5ac335081d1..b70f951a784 100644 --- a/encoders/protoc-gen-firebase-encoders/tests/tests.gradle +++ b/encoders/protoc-gen-firebase-encoders/tests/tests.gradle @@ -49,7 +49,7 @@ protobuf { dependencies { testImplementation project(":encoders:firebase-encoders") testImplementation project(":encoders:firebase-encoders-proto") - testImplementation "com.google.protobuf:protobuf-java:3.14.0" + testImplementation "com.google.protobuf:protobuf-java:3.21.9" testImplementation 'junit:junit:4.13.1' testImplementation "com.google.truth:truth:1.0.1" diff --git a/firebase-abt/gradle.properties b/firebase-abt/gradle.properties index f8301e25bcc..fd551588b1b 100644 --- a/firebase-abt/gradle.properties +++ b/firebase-abt/gradle.properties @@ -1,2 +1,2 @@ -version=21.0.3 -latestReleasedVersion=21.0.2 +version=21.1.1 +latestReleasedVersion=21.1.0 diff --git a/firebase-annotations/firebase-annotations.gradle b/firebase-annotations/firebase-annotations.gradle.kts similarity index 68% rename from firebase-annotations/firebase-annotations.gradle rename to firebase-annotations/firebase-annotations.gradle.kts index e77114c5afb..eb5d1a2ba3d 100644 --- a/firebase-annotations/firebase-annotations.gradle +++ b/firebase-annotations/firebase-annotations.gradle.kts @@ -13,19 +13,22 @@ // limitations under the License. plugins { - id 'firebase-java-library' + id("firebase-java-library") } firebaseLibrary { - publishSources = true - publishJavadoc = false + publishSources = true + publishJavadoc = false } java { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} +tasks.withType { + options.compilerArgs.add("-Werror") } -tasks.withType(JavaCompile) { - options.compilerArgs << "-Werror" +dependencies { + implementation(libs.javax.inject) } diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java new file mode 100644 index 00000000000..5626ea94a78 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Background.java @@ -0,0 +1,30 @@ +// Copyright 2022 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.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for long running tasks including disk IO, heavy CPU + * computations. + * + *

For operations that can block for long periods of time, like network requests, use the {@link + * Blocking} executor. + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Background {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java new file mode 100644 index 00000000000..d57513b57f1 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Blocking.java @@ -0,0 +1,27 @@ +// Copyright 2022 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.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for tasks that can block for long periods of time, e.g network + * IO. + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Blocking {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java new file mode 100644 index 00000000000..4d3b0828954 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/Lightweight.java @@ -0,0 +1,26 @@ +// Copyright 2022 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.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * An executor/coroutine dispatcher for lightweight tasks that never block (on IO or other tasks). + */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface Lightweight {} diff --git a/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java new file mode 100644 index 00000000000..bad10c164b8 --- /dev/null +++ b/firebase-annotations/src/main/java/com/google/firebase/annotations/concurrent/UiThread.java @@ -0,0 +1,24 @@ +// Copyright 2022 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.annotations.concurrent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** An executor/coroutine dispatcher for work that must run on the UI thread. */ +@Qualifier +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface UiThread {} diff --git a/firebase-appdistribution-api/ktx/src/androidTest/kotlin/com/google/firebase/app/distribution/ktx/FirebaseAppDistributionTests.kt b/firebase-appdistribution-api/ktx/src/androidTest/kotlin/com/google/firebase/app/distribution/ktx/FirebaseAppDistributionTests.kt index 9b0ae3881b1..d6e24db0c31 100644 --- a/firebase-appdistribution-api/ktx/src/androidTest/kotlin/com/google/firebase/app/distribution/ktx/FirebaseAppDistributionTests.kt +++ b/firebase-appdistribution-api/ktx/src/androidTest/kotlin/com/google/firebase/app/distribution/ktx/FirebaseAppDistributionTests.kt @@ -40,86 +40,88 @@ const val EXISTING_APP = "existing" @RunWith(AndroidJUnit4ClassRunner::class) abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(AndroidJUnit4ClassRunner::class) class FirebaseAppDistributionTests : BaseTestCase() { - @Test - fun appDistribution_default_callsDefaultGetInstance() { - assertThat(Firebase.appDistribution).isSameInstanceAs(FirebaseAppDistribution.getInstance()) - } + @Test + fun appDistribution_default_callsDefaultGetInstance() { + assertThat(Firebase.appDistribution).isSameInstanceAs(FirebaseAppDistribution.getInstance()) + } - @Test - fun appDistributionReleaseDestructuringDeclarationsWork() { - val mockAppDistributionRelease = object : AppDistributionRelease { - override fun getDisplayVersion(): String = "1.0.0" + @Test + fun appDistributionReleaseDestructuringDeclarationsWork() { + val mockAppDistributionRelease = + object : AppDistributionRelease { + override fun getDisplayVersion(): String = "1.0.0" - override fun getVersionCode(): Long = 1L + override fun getVersionCode(): Long = 1L - override fun getReleaseNotes(): String = "Changelog..." + override fun getReleaseNotes(): String = "Changelog..." - override fun getBinaryType(): BinaryType = BinaryType.AAB - } + override fun getBinaryType(): BinaryType = BinaryType.AAB + } - val (type, displayVersion, versionCode, notes) = mockAppDistributionRelease + val (type, displayVersion, versionCode, notes) = mockAppDistributionRelease - assertThat(type).isEqualTo(mockAppDistributionRelease.binaryType) - assertThat(displayVersion).isEqualTo(mockAppDistributionRelease.displayVersion) - assertThat(versionCode).isEqualTo(mockAppDistributionRelease.versionCode) - assertThat(notes).isEqualTo(mockAppDistributionRelease.releaseNotes) - } + assertThat(type).isEqualTo(mockAppDistributionRelease.binaryType) + assertThat(displayVersion).isEqualTo(mockAppDistributionRelease.displayVersion) + assertThat(versionCode).isEqualTo(mockAppDistributionRelease.versionCode) + assertThat(notes).isEqualTo(mockAppDistributionRelease.releaseNotes) + } - @Test - fun updateProgressDestructuringDeclarationsWork() { - val mockUpdateProgress = object : UpdateProgress { - override fun getApkBytesDownloaded(): Long = 1200L + @Test + fun updateProgressDestructuringDeclarationsWork() { + val mockUpdateProgress = + object : UpdateProgress { + override fun getApkBytesDownloaded(): Long = 1200L - override fun getApkFileTotalBytes(): Long = 9000L + override fun getApkFileTotalBytes(): Long = 9000L - override fun getUpdateStatus(): UpdateStatus = UpdateStatus.DOWNLOADING - } + override fun getUpdateStatus(): UpdateStatus = UpdateStatus.DOWNLOADING + } - val (downloaded, total, status) = mockUpdateProgress + val (downloaded, total, status) = mockUpdateProgress - assertThat(downloaded).isEqualTo(mockUpdateProgress.apkBytesDownloaded) - assertThat(total).isEqualTo(mockUpdateProgress.apkFileTotalBytes) - assertThat(status).isEqualTo(mockUpdateProgress.updateStatus) - } + assertThat(downloaded).isEqualTo(mockUpdateProgress.apkBytesDownloaded) + assertThat(total).isEqualTo(mockUpdateProgress.apkFileTotalBytes) + assertThat(status).isEqualTo(mockUpdateProgress.updateStatus) + } } internal const val LIBRARY_NAME: String = "fire-appdistribution-ktx" @RunWith(AndroidJUnit4ClassRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun libraryRegistrationAtRuntime() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt b/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt index ced4d2963f5..cec2a11f5f5 100644 --- a/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt +++ b/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt @@ -27,7 +27,7 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseAppDistribution] instance of the default [FirebaseApp]. */ val Firebase.appDistribution: FirebaseAppDistribution - get() = FirebaseAppDistribution.getInstance() + get() = FirebaseAppDistribution.getInstance() /** * Destructuring declaration for [AppDistributionRelease] to provide binaryType. @@ -83,6 +83,6 @@ internal const val LIBRARY_NAME: String = "fire-appdistribution-ktx" /** @suppress */ @Keep class FirebaseAppDistributionKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionStub.java b/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionStub.java index b2e4ddf863d..a8e8f5f73d8 100644 --- a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionStub.java +++ b/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionStub.java @@ -14,6 +14,7 @@ package com.google.firebase.appdistribution.internal; +import android.annotation.SuppressLint; import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -38,6 +39,8 @@ * Tasks}/{@link UpdateTask UpdateTasks} with {@link * FirebaseAppDistributionException.Status#NOT_IMPLEMENTED}. */ +// TODO(b/261013680): Use an explicit executor in continuations. +@SuppressLint("TaskMainThread") public class FirebaseAppDistributionStub implements FirebaseAppDistribution { @NonNull @Override diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AabUpdater.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AabUpdater.java index cc4122d7d0d..5c1cbfd2db9 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AabUpdater.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AabUpdater.java @@ -19,6 +19,7 @@ import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask; import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.net.Uri; @@ -52,6 +53,8 @@ class AabUpdater { @GuardedBy("updateAabLock") private boolean hasBeenSentToPlayForCurrentTask = false; + // TODO(b/261014422): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") AabUpdater() { this( FirebaseAppDistributionLifecycleNotifier.getInstance(), diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkUpdater.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkUpdater.java index 3945b4234e5..cd06805977e 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkUpdater.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkUpdater.java @@ -19,6 +19,7 @@ import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException; import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; @@ -62,6 +63,8 @@ class ApkUpdater { private final Object updateTaskLock = new Object(); + // TODO(b/261014422): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ApkUpdater(@NonNull FirebaseApp firebaseApp, @NonNull ApkInstaller apkInstaller) { this( Executors.newSingleThreadExecutor(), diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java index 01c72cd2a1c..0a472ef6947 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java @@ -21,6 +21,7 @@ import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException; import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -101,6 +102,8 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution { @Override @NonNull + // TODO(b/261014422): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public UpdateTask updateIfNewReleaseAvailable() { synchronized (updateIfNewReleaseTaskLock) { if (updateIfNewReleaseAvailableIsTaskInProgress()) { @@ -111,7 +114,6 @@ public UpdateTask updateIfNewReleaseAvailable() { remakeUpdateConfirmationDialog = false; dialogHostActivity = null; } - lifecycleNotifier .applyToForegroundActivityTask(this::showSignInConfirmationDialog) .onSuccessTask(unused -> signInTester()) @@ -219,6 +221,8 @@ public void signOutTester() { @Override @NonNull + // TODO(b/261014422): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public synchronized Task checkForNewRelease() { if (cachedCheckForNewReleaseTask != null && !cachedCheckForNewReleaseTask.isComplete()) { LogWrapper.getInstance().v("Response in progress"); diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionTesterApiClient.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionTesterApiClient.java index f5dfd8e4173..1e0734b365a 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionTesterApiClient.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionTesterApiClient.java @@ -16,6 +16,7 @@ import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; @@ -63,6 +64,9 @@ private interface FidDependentJob { private final FirebaseApp firebaseApp; private final Provider firebaseInstallationsApiProvider; private final TesterApiHttpClient testerApiHttpClient; + + // TODO(b/261014422): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private final Executor taskExecutor = Executors.newSingleThreadExecutor(); FirebaseAppDistributionTesterApiClient( diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/NewReleaseFetcher.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/NewReleaseFetcher.java index 8ef56f6a7b2..55df650213d 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/NewReleaseFetcher.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/NewReleaseFetcher.java @@ -16,6 +16,7 @@ import static com.google.firebase.appdistribution.impl.PackageInfoUtils.getPackageInfo; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -48,6 +49,8 @@ class NewReleaseFetcher { this.releaseIdentifier = releaseIdentifier; } + // TODO(b/261014422): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull public synchronized Task checkForNewRelease() { if (cachedCheckForNewRelease != null && !cachedCheckForNewRelease.isComplete()) { diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterSignInManager.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterSignInManager.java index 63f4b83ad93..2034b27c71a 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterSignInManager.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterSignInManager.java @@ -18,6 +18,7 @@ import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException; import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -113,6 +114,8 @@ void onActivityResumed(Activity activity) { } } + // TODO(b/261014422): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull public Task signInTester() { if (signInStorage.getSignInStatus()) { diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/UpdateTaskImpl.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/UpdateTaskImpl.java index ecd55f0dc4b..b0f523951d4 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/UpdateTaskImpl.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/UpdateTaskImpl.java @@ -34,6 +34,7 @@ import java.util.concurrent.Executor; /** Implementation of UpdateTask, the return type of updateApp. */ +// TODO(b/261013814): Use an explicit executor in continuations. class UpdateTaskImpl extends UpdateTask { @Nullable @GuardedBy("lock") diff --git a/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/MainActivity.kt b/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/MainActivity.kt index 8165362dacd..5d12271e194 100644 --- a/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/MainActivity.kt +++ b/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/MainActivity.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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.googletest.firebase.appdistribution.testapp import android.app.AlertDialog diff --git a/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt b/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt index 7ebc5968e0e..8af6529e3d5 100644 --- a/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt +++ b/firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt @@ -1,3 +1,17 @@ +// Copyright 2022 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.googletest.firebase.appdistribution.testapp import android.content.Intent diff --git a/firebase-common/data-collection-tests/data-collection-tests.gradle b/firebase-common/data-collection-tests/data-collection-tests.gradle deleted file mode 100644 index b690f166c85..00000000000 --- a/firebase-common/data-collection-tests/data-collection-tests.gradle +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'com.android.application' -} - -android { - adbOptions { - timeOutInMs 60 * 1000 - } - - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - versionName version - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - testImplementation 'androidx.test:runner:1.2.0' - testImplementation 'androidx.test.ext:junit:1.1.1' - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'com.google.auto.value:auto-value-annotations:1.6.5' - testImplementation 'junit:junit:4.12' - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'org.mockito:mockito-core:2.25.0' - implementation 'androidx.core:core:1.2.0' -} diff --git a/firebase-common/data-collection-tests/data-collection-tests.gradle.kts b/firebase-common/data-collection-tests/data-collection-tests.gradle.kts new file mode 100644 index 00000000000..5ae16ee552c --- /dev/null +++ b/firebase-common/data-collection-tests/data-collection-tests.gradle.kts @@ -0,0 +1,47 @@ +// Copyright 2022 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("com.android.application") +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + compileSdk = targetSdkVersion + defaultConfig { + minSdk = minSdkVersion + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.robolectric) + testImplementation(libs.autovalue.annotations) + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.mockito.core) + testImplementation(libs.androidx.core) +} diff --git a/firebase-common/firebase-common.gradle b/firebase-common/firebase-common.gradle deleted file mode 100644 index ac5c0a9c218..00000000000 --- a/firebase-common/firebase-common.gradle +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' -} - -firebaseLibrary { - testLab.enabled = true - publishSources = true -} - -android { - adbOptions { - timeOutInMs 60 * 1000 - } - - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - versionName version - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard.txt' - } - sourceSets { - androidTest { - java { - srcDir 'src/testUtil' - } - } - test { - java { - srcDir 'src/testUtil' - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } - lintOptions { - baseline file("lint-baseline.xml") - } -} - -dependencies { - // TODO(vkryachko): have sdks depend on components directly once components are released. - implementation project(':firebase-components') - implementation 'com.google.android.gms:play-services-basement:18.1.0' - implementation "com.google.android.gms:play-services-tasks:18.0.1" - - // FirebaseApp references storage, so storage needs to be on classpath when dokka runs. - javadocClasspath project(path: ':firebase-storage') - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' - - compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6' - compileOnly 'com.google.code.findbugs:jsr305:3.0.2' - - // needed for Kotlin detection to compile, but not necessarily present at runtime. - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - - testImplementation 'androidx.test:runner:1.2.0' - testImplementation 'androidx.test.ext:junit:1.1.1' - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.12' - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation 'androidx.test:core:1.2.0' - testImplementation 'org.json:json:20210307' - - annotationProcessor 'com.google.auto.value:auto-value:1.6.5' - - androidTestImplementation project(':integ-testing') - androidTestImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation "com.google.truth:truth:$googleTruthVersion" - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.28.1' - androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.28.1' -} diff --git a/firebase-common/firebase-common.gradle.kts b/firebase-common/firebase-common.gradle.kts new file mode 100644 index 00000000000..d9f0ef55095 --- /dev/null +++ b/firebase-common/firebase-common.gradle.kts @@ -0,0 +1,84 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +plugins { + id("firebase-library") +} + +firebaseLibrary { + testLab.enabled = true + publishSources = true +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + + compileSdk = targetSdkVersion + defaultConfig { + minSdk = minSdkVersion + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("proguard.txt") + } + sourceSets { + getByName("androidTest") { + java.srcDirs("src/testUtil") + } + getByName("test") { + java.srcDirs("src/testUtil") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-annotations")) + implementation(project(":firebase-components")) + implementation(libs.androidx.futures) + implementation(libs.playservices.basement) + implementation(libs.playservices.tasks) + + annotationProcessor(libs.autovalue) + + compileOnly(libs.autovalue.annotations) + compileOnly(libs.findbugs.jsr305) + // needed for Kotlin detection to compile, but not necessarily present at runtime. + compileOnly(libs.kotlin.stdlib) + + // FirebaseApp references storage, so storage needs to be on classpath when dokka runs. + javadocClasspath(project(":firebase-storage")) + + testImplementation(libs.androidx.test.junit) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.org.json) + testImplementation(libs.robolectric) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.mockito.core) + androidTestImplementation(libs.mockito.dexmaker) + androidTestImplementation(libs.truth) + androidTestImplementation(project(":integ-testing")) +} diff --git a/firebase-common/gradle.properties b/firebase-common/gradle.properties index 18ebccb64b0..165d1ccbf35 100644 --- a/firebase-common/gradle.properties +++ b/firebase-common/gradle.properties @@ -1,3 +1,3 @@ -version=20.1.3 -latestReleasedVersion=20.1.2 +version=20.2.1 +latestReleasedVersion=20.2.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-common/ktx/ktx.gradle b/firebase-common/ktx/ktx.gradle deleted file mode 100644 index 2d15573ab83..00000000000 --- a/firebase-common/ktx/ktx.gradle +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' - id 'kotlin-android' -} - -firebaseLibrary { - releaseWith project(':firebase-common') -} - -android { - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - versionName version - } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - test.java.srcDirs += 'src/test/kotlin' - } - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation 'androidx.annotation:annotation:1.1.0' - - // We're exposing this library as a transitive dependency so developers can - // get Kotlin Coroutines support out-of-the-box for methods that return a Task - api "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion" - - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.12' - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" -} diff --git a/firebase-common/ktx/ktx.gradle.kts b/firebase-common/ktx/ktx.gradle.kts new file mode 100644 index 00000000000..f123fb5e25c --- /dev/null +++ b/firebase-common/ktx/ktx.gradle.kts @@ -0,0 +1,60 @@ +// Copyright 2022 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("kotlin-android") +} + +firebaseLibrary { + releaseWith(project(":firebase-common")) +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + compileSdk = targetSdkVersion + defaultConfig { + minSdk = minSdkVersion + targetSdk = targetSdkVersion + } + sourceSets { + getByName("main") { + java.srcDirs("src/main/kotlin") + } + getByName("test") { + java.srcDirs("src/test/kotlin") + } + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(libs.kotlin.stdlib) + + implementation(project(":firebase-annotations")) + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) + implementation(libs.androidx.annotation) + + // We"re exposing this library as a transitive dependency so developers can + // get Kotlin Coroutines support out-of-the-box for methods that return a Task + api(libs.kotlin.coroutines.tasks) + + testImplementation(libs.robolectric) + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.androidx.test.core) + testImplementation(libs.kotlin.coroutines.test) +} diff --git a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt b/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt index f939f218f0f..058c0f20626 100644 --- a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt +++ b/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Firebase.kt @@ -17,9 +17,18 @@ import android.content.Context import androidx.annotation.Keep import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread import com.google.firebase.components.Component import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified import com.google.firebase.platforminfo.LibraryVersionComponent +import java.util.concurrent.Executor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher /** * Single access point to all firebase SDKs from Kotlin. @@ -30,7 +39,7 @@ object Firebase /** Returns the default firebase app instance. */ val Firebase.app: FirebaseApp - get() = FirebaseApp.getInstance() + get() = FirebaseApp.getInstance() /** Returns a named firebase app instance. */ fun Firebase.app(name: String): FirebaseApp = FirebaseApp.getInstance(name) @@ -40,23 +49,36 @@ fun Firebase.initialize(context: Context): FirebaseApp? = FirebaseApp.initialize /** Initializes and returns a FirebaseApp. */ fun Firebase.initialize(context: Context, options: FirebaseOptions): FirebaseApp = - FirebaseApp.initializeApp(context, options) + FirebaseApp.initializeApp(context, options) /** Initializes and returns a FirebaseApp. */ fun Firebase.initialize(context: Context, options: FirebaseOptions, name: String): FirebaseApp = - FirebaseApp.initializeApp(context, options, name) + FirebaseApp.initializeApp(context, options, name) /** Returns options of default FirebaseApp */ val Firebase.options: FirebaseOptions - get() = Firebase.app.options + get() = Firebase.app.options internal const val LIBRARY_NAME: String = "fire-core-ktx" /** @suppress */ @Keep class FirebaseCommonKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return listOf( - LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) - } + override fun getComponents(): List> { + return listOf( + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), + coroutineDispatcher(), + coroutineDispatcher(), + coroutineDispatcher(), + coroutineDispatcher() + ) + } } + +private inline fun coroutineDispatcher(): Component = + Component.builder(Qualified.qualified(T::class.java, CoroutineDispatcher::class.java)) + .add(Dependency.required(Qualified.qualified(T::class.java, Executor::class.java))) + .factory { c -> + c.get(Qualified.qualified(T::class.java, Executor::class.java)).asCoroutineDispatcher() + } + .build() diff --git a/firebase-common/ktx/src/test/kotlin/com/google/firebase/ktx/Tests.kt b/firebase-common/ktx/src/test/kotlin/com/google/firebase/ktx/Tests.kt index 45bcaece248..43ac45c33b3 100644 --- a/firebase-common/ktx/src/test/kotlin/com/google/firebase/ktx/Tests.kt +++ b/firebase-common/ktx/src/test/kotlin/com/google/firebase/ktx/Tests.kt @@ -28,112 +28,115 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner fun withApp(name: String, block: FirebaseApp.() -> Unit) { - val app = Firebase.initialize(ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId("appId") - .build(), - name) - try { - block(app) - } finally { - app.delete() - } + val app = + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder().setApplicationId("appId").build(), + name + ) + try { + block(app) + } finally { + app.delete() + } } class TestException(message: String) : Exception(message) @RunWith(RobolectricTestRunner::class) class VersionTests { - @Test - fun libraryVersions_shouldBeRegisteredWithRuntime() { - withApp("ktxTestApp") { - val uaPublisher = get(UserAgentPublisher::class.java) - assertThat(uaPublisher.userAgent).contains("kotlin") - assertThat(uaPublisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun libraryVersions_shouldBeRegisteredWithRuntime() { + withApp("ktxTestApp") { + val uaPublisher = get(UserAgentPublisher::class.java) + assertThat(uaPublisher.userAgent).contains("kotlin") + assertThat(uaPublisher.userAgent).contains(LIBRARY_NAME) } + } } @RunWith(RobolectricTestRunner::class) class KtxTests { - @Test - fun `Firebase#app should delegate to FirebaseApp#getInstance()`() { - withApp(FirebaseApp.DEFAULT_APP_NAME) { - assertThat(Firebase.app).isSameInstanceAs(FirebaseApp.getInstance()) - } + @Test + fun `Firebase#app should delegate to FirebaseApp#getInstance()`() { + withApp(FirebaseApp.DEFAULT_APP_NAME) { + assertThat(Firebase.app).isSameInstanceAs(FirebaseApp.getInstance()) } + } - @Test - fun `Firebase#app(String) should delegate to FirebaseApp#getInstance(String)`() { - val appName = "testApp" - withApp(appName) { - assertThat(Firebase.app(appName)).isSameInstanceAs(FirebaseApp.getInstance(appName)) - } + @Test + fun `Firebase#app(String) should delegate to FirebaseApp#getInstance(String)`() { + val appName = "testApp" + withApp(appName) { + assertThat(Firebase.app(appName)).isSameInstanceAs(FirebaseApp.getInstance(appName)) } + } - @Test - fun `Firebase#options should delegate to FirebaseApp#getInstance()#options`() { - withApp(FirebaseApp.DEFAULT_APP_NAME) { - assertThat(Firebase.options).isSameInstanceAs(FirebaseApp.getInstance().options) - } + @Test + fun `Firebase#options should delegate to FirebaseApp#getInstance()#options`() { + withApp(FirebaseApp.DEFAULT_APP_NAME) { + assertThat(Firebase.options).isSameInstanceAs(FirebaseApp.getInstance().options) } + } - @Test - fun `Firebase#initialize(Context, FirebaseOptions) should initialize the app correctly`() { - val options = FirebaseOptions.Builder().setApplicationId("appId").build() - val app = Firebase.initialize(ApplicationProvider.getApplicationContext(), options) - try { - assertThat(app).isNotNull() - assertThat(app.name).isEqualTo(FirebaseApp.DEFAULT_APP_NAME) - assertThat(app.options).isSameInstanceAs(options) - assertThat(app.applicationContext).isSameInstanceAs(ApplicationProvider.getApplicationContext()) - } finally { - app.delete() - } + @Test + fun `Firebase#initialize(Context, FirebaseOptions) should initialize the app correctly`() { + val options = FirebaseOptions.Builder().setApplicationId("appId").build() + val app = Firebase.initialize(ApplicationProvider.getApplicationContext(), options) + try { + assertThat(app).isNotNull() + assertThat(app.name).isEqualTo(FirebaseApp.DEFAULT_APP_NAME) + assertThat(app.options).isSameInstanceAs(options) + assertThat(app.applicationContext) + .isSameInstanceAs(ApplicationProvider.getApplicationContext()) + } finally { + app.delete() } + } - @Test - fun `Firebase#initialize(Context, FirebaseOptions, String) should initialize the app correctly`() { - val options = FirebaseOptions.Builder().setApplicationId("appId").build() - val name = "appName" - val app = Firebase.initialize(ApplicationProvider.getApplicationContext(), options, name) - try { - assertThat(app).isNotNull() - assertThat(app.name).isEqualTo(name) - assertThat(app.options).isSameInstanceAs(options) - assertThat(app.applicationContext).isSameInstanceAs(ApplicationProvider.getApplicationContext()) - } finally { - app.delete() - } + @Test + fun `Firebase#initialize(Context, FirebaseOptions, String) should initialize the app correctly`() { + val options = FirebaseOptions.Builder().setApplicationId("appId").build() + val name = "appName" + val app = Firebase.initialize(ApplicationProvider.getApplicationContext(), options, name) + try { + assertThat(app).isNotNull() + assertThat(app.name).isEqualTo(name) + assertThat(app.options).isSameInstanceAs(options) + assertThat(app.applicationContext) + .isSameInstanceAs(ApplicationProvider.getApplicationContext()) + } finally { + app.delete() } + } } class CoroutinesPlayServicesTests { - // We are only interested in the await() function offered by kotlinx-coroutines-play-services - // So we're not testing the other functions provided by that library. + // We are only interested in the await() function offered by kotlinx-coroutines-play-services + // So we're not testing the other functions provided by that library. - @Test - fun `Task#await() resolves to the same result as Task#getResult()`() = runTest { - val task = Tasks.forResult(21) + @Test + fun `Task#await() resolves to the same result as Task#getResult()`() = runTest { + val task = Tasks.forResult(21) - val expected = task.result - val actual = task.await() + val expected = task.result + val actual = task.await() - assertThat(actual).isEqualTo(expected) - assertThat(task.isSuccessful).isTrue() - assertThat(task.exception).isNull() - } + assertThat(actual).isEqualTo(expected) + assertThat(task.isSuccessful).isTrue() + assertThat(task.exception).isNull() + } - @Test - fun `Task#await() throws an Exception for failing Tasks`() = runTest { - val task = Tasks.forException(TestException("some error happened")) - - try { - task.await() - fail("Task#await should throw an Exception") - } catch (e: Exception) { - assertThat(e).isInstanceOf(TestException::class.java) - assertThat(task.isSuccessful).isFalse() - } + @Test + fun `Task#await() throws an Exception for failing Tasks`() = runTest { + val task = Tasks.forException(TestException("some error happened")) + + try { + task.await() + fail("Task#await should throw an Exception") + } catch (e: Exception) { + assertThat(e).isInstanceOf(TestException::class.java) + assertThat(task.isSuccessful).isFalse() } + } } diff --git a/firebase-common/lint-baseline.xml b/firebase-common/lint-baseline.xml deleted file mode 100644 index ea62ca50d4b..00000000000 --- a/firebase-common/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java index efc6c01d8ce..ee16ff412e6 100644 --- a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java +++ b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java @@ -23,8 +23,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.text.TextUtils; import android.util.Log; import androidx.annotation.GuardedBy; @@ -47,10 +45,12 @@ import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.ComponentRuntime; import com.google.firebase.components.Lazy; +import com.google.firebase.concurrent.ExecutorsRegistrar; import com.google.firebase.events.Publisher; import com.google.firebase.heartbeatinfo.DefaultHeartBeatController; import com.google.firebase.inject.Provider; import com.google.firebase.internal.DataCollectionConfigStorage; +import com.google.firebase.provider.FirebaseInitProvider; import com.google.firebase.tracing.ComponentMonitor; import com.google.firebase.tracing.FirebaseTrace; import java.nio.charset.Charset; @@ -59,7 +59,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -97,16 +96,10 @@ public class FirebaseApp { private static final Object LOCK = new Object(); - private static final Executor UI_EXECUTOR = new UiExecutor(); - /** A map of (name, FirebaseApp) instances. */ @GuardedBy("LOCK") static final Map INSTANCES = new ArrayMap<>(); - private static final String FIREBASE_ANDROID = "fire-android"; - private static final String FIREBASE_COMMON = "fire-core"; - private static final String KOTLIN = "kotlin"; - private final Context applicationContext; private final String name; private final FirebaseOptions options; @@ -416,6 +409,7 @@ protected FirebaseApp(Context applicationContext, String name, FirebaseOptions o this.applicationContext = Preconditions.checkNotNull(applicationContext); this.name = Preconditions.checkNotEmpty(name); this.options = Preconditions.checkNotNull(options); + StartupTime startupTime = FirebaseInitProvider.getStartupTime(); FirebaseTrace.pushTrace("Firebase"); @@ -426,15 +420,23 @@ protected FirebaseApp(Context applicationContext, String name, FirebaseOptions o FirebaseTrace.popTrace(); // ComponentDiscovery FirebaseTrace.pushTrace("Runtime"); - componentRuntime = - ComponentRuntime.builder(UI_EXECUTOR) + ComponentRuntime.Builder builder = + ComponentRuntime.builder(com.google.firebase.concurrent.UiExecutor.INSTANCE) .addLazyComponentRegistrars(registrars) .addComponentRegistrar(new FirebaseCommonRegistrar()) + .addComponentRegistrar(new ExecutorsRegistrar()) .addComponent(Component.of(applicationContext, Context.class)) .addComponent(Component.of(this, FirebaseApp.class)) .addComponent(Component.of(options, FirebaseOptions.class)) - .setProcessor(new ComponentMonitor()) - .build(); + .setProcessor(new ComponentMonitor()); + + // Don't provide StartupTime in direct boot mode or if Firebase was manually started + if (UserManagerCompat.isUserUnlocked(applicationContext) + && FirebaseInitProvider.isCurrentlyInitializing()) { + builder.addComponent(Component.of(startupTime, StartupTime.class)); + } + + componentRuntime = builder.build(); FirebaseTrace.popTrace(); // Runtime dataCollectionConfigStorage = @@ -712,14 +714,4 @@ public void onBackgroundStateChanged(boolean background) { } } } - - private static class UiExecutor implements Executor { - - private static final Handler HANDLER = new Handler(Looper.getMainLooper()); - - @Override - public void execute(@NonNull Runnable command) { - HANDLER.post(command); - } - } } diff --git a/firebase-common/src/main/java/com/google/firebase/StartupTime.java b/firebase-common/src/main/java/com/google/firebase/StartupTime.java new file mode 100644 index 00000000000..972b0ed3c2c --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/StartupTime.java @@ -0,0 +1,56 @@ +// Copyright 2022 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; + +import android.os.SystemClock; +import androidx.annotation.NonNull; +import com.google.auto.value.AutoValue; + +/** + * Represents Firebase's startup time in several timing methods. Represented in unix time/epoch + * milliseconds milliseconds since boot, and uptime milliseconds. The absence of a StartupTime + * indicates an unreliable or misleading time, such as a launch in direct boot mode. Because of + * this, StartupTime cannot be guaranteed to be present, and instead should be optionally depended + * on, and its absence handled. + * + * @hide + */ +@AutoValue +public abstract class StartupTime { + + /** @return The epoch time that Firebase began initializing, in milliseconds */ + public abstract long getEpochMillis(); + + /** @return The number of milliseconds from boot to when Firebase began initializing */ + public abstract long getElapsedRealtime(); + + /** @return The number of milliseconds of uptime measured by SystemClock.uptimeMillis() */ + public abstract long getUptimeMillis(); + + /** + * @param epochMillis Time in milliseconds since epoch + * @param elapsedRealtime Time in milliseconds since boot + */ + public static @NonNull StartupTime create( + long epochMillis, long elapsedRealtime, long uptimeMillis) { + return new AutoValue_StartupTime(epochMillis, elapsedRealtime, uptimeMillis); + } + + /** @return A StartupTime represented by the current epoch time and JVM nano time */ + public static @NonNull StartupTime now() { + return create( + System.currentTimeMillis(), SystemClock.elapsedRealtime(), SystemClock.uptimeMillis()); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java b/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java new file mode 100644 index 00000000000..417435c2abf --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/CustomThreadFactory.java @@ -0,0 +1,53 @@ +// Copyright 2022 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.concurrent; + +import android.os.Process; +import android.os.StrictMode; +import java.util.Locale; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; + +class CustomThreadFactory implements ThreadFactory { + private static final ThreadFactory DEFAULT = Executors.defaultThreadFactory(); + private final AtomicLong threadCount = new AtomicLong(); + private final String namePrefix; + private final int priority; + private final StrictMode.ThreadPolicy policy; + + CustomThreadFactory(String namePrefix, int priority, @Nullable StrictMode.ThreadPolicy policy) { + this.namePrefix = namePrefix; + this.priority = priority; + this.policy = policy; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = + DEFAULT.newThread( + () -> { + Process.setThreadPriority(priority); + if (policy != null) { + StrictMode.setThreadPolicy(policy); + } + r.run(); + }); + thread.setName( + String.format(Locale.ROOT, "%s Thread #%d", namePrefix, threadCount.getAndIncrement())); + return thread; + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java new file mode 100644 index 00000000000..23b9385c284 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledExecutorService.java @@ -0,0 +1,185 @@ +// Copyright 2022 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.concurrent; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class DelegatingScheduledExecutorService implements ScheduledExecutorService { + private final ExecutorService delegate; + private final ScheduledExecutorService scheduler; + + DelegatingScheduledExecutorService(ExecutorService delegate, ScheduledExecutorService scheduler) { + this.delegate = delegate; + this.scheduler = scheduler; + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException("Shutting down is not allowed."); + } + + @Override + public List shutdownNow() { + throw new UnsupportedOperationException("Shutting down is not allowed."); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return delegate.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return delegate.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return delegate.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws ExecutionException, InterruptedException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException { + return delegate.invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + delegate.execute(command); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture( + completer -> + scheduler.schedule( + () -> + delegate.execute( + () -> { + try { + command.run(); + completer.set(null); + } catch (Exception ex) { + completer.setException(ex); + } + }), + delay, + unit)); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.schedule( + () -> + delegate.submit( + () -> { + try { + V result = callable.call(); + completer.set(result); + } catch (Exception ex) { + completer.setException(ex); + } + }), + delay, + unit)); + } + + @Override + public ScheduledFuture scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.scheduleAtFixedRate( + () -> + delegate.execute( + () -> { + try { + command.run(); + } catch (Exception ex) { + completer.setException(ex); + throw ex; + } + }), + initialDelay, + period, + unit)); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + return new DelegatingScheduledFuture<>( + completer -> + scheduler.scheduleWithFixedDelay( + () -> + delegate.execute( + () -> { + try { + command.run(); + } catch (Exception ex) { + completer.setException(ex); + } + }), + initialDelay, + delay, + unit)); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java new file mode 100644 index 00000000000..26ef258136a --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/DelegatingScheduledFuture.java @@ -0,0 +1,72 @@ +// Copyright 2022 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.concurrent; + +import android.annotation.SuppressLint; +import androidx.concurrent.futures.AbstractResolvableFuture; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +// While direct use of AbstractResolvableFuture is not encouraged, it's stable for use and is not +// going to be removed. In this case it's required since we need to implement a ScheduledFuture so +// we can't use CallbackToFutureAdapter. +@SuppressLint("RestrictedApi") +class DelegatingScheduledFuture extends AbstractResolvableFuture + implements ScheduledFuture { + + interface Completer { + void set(T value); + + void setException(Throwable ex); + } + + interface Resolver { + ScheduledFuture addCompleter(Completer completer); + } + + DelegatingScheduledFuture(Resolver resolver) { + upstreamFuture = + resolver.addCompleter( + new Completer() { + @Override + public void set(V value) { + DelegatingScheduledFuture.this.set(value); + } + + @Override + public void setException(Throwable ex) { + DelegatingScheduledFuture.this.setException(ex); + } + }); + } + + private final ScheduledFuture upstreamFuture; + + @Override + protected void afterDone() { + upstreamFuture.cancel(wasInterrupted()); + } + + @Override + public long getDelay(TimeUnit unit) { + return upstreamFuture.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return upstreamFuture.compareTo(o); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java b/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java new file mode 100644 index 00000000000..0641428fdb0 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/ExecutorsRegistrar.java @@ -0,0 +1,133 @@ +// Copyright 2022 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.concurrent; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Process; +import android.os.StrictMode; +import com.google.firebase.BuildConfig; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Lazy; +import com.google.firebase.components.Qualified; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +@SuppressLint("ThreadPoolCreation") +public class ExecutorsRegistrar implements ComponentRegistrar { + static final Lazy BG_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newFixedThreadPool( + 4, + factory( + "Firebase Background", Process.THREAD_PRIORITY_BACKGROUND, bgPolicy())))); + + static final Lazy LITE_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newFixedThreadPool( + Math.max(2, Runtime.getRuntime().availableProcessors()), + factory("Firebase Lite", Process.THREAD_PRIORITY_DEFAULT, litePolicy())))); + + static final Lazy BLOCKING_EXECUTOR = + new Lazy<>( + () -> + scheduled( + Executors.newCachedThreadPool( + factory( + "Firebase Blocking", + Process.THREAD_PRIORITY_BACKGROUND + + Process.THREAD_PRIORITY_LESS_FAVORABLE)))); + + private static final Lazy SCHEDULER = + new Lazy<>( + () -> + Executors.newSingleThreadScheduledExecutor( + factory("Firebase Scheduler", Process.THREAD_PRIORITY_DEFAULT))); + + @Override + public List> getComponents() { + return Arrays.asList( + Component.builder( + Qualified.qualified(Background.class, ScheduledExecutorService.class), + Qualified.qualified(Background.class, ExecutorService.class), + Qualified.qualified(Background.class, Executor.class)) + .factory(c -> BG_EXECUTOR.get()) + .build(), + Component.builder( + Qualified.qualified(Blocking.class, ScheduledExecutorService.class), + Qualified.qualified(Blocking.class, ExecutorService.class), + Qualified.qualified(Blocking.class, Executor.class)) + .factory(c -> BLOCKING_EXECUTOR.get()) + .build(), + Component.builder( + Qualified.qualified(Lightweight.class, ScheduledExecutorService.class), + Qualified.qualified(Lightweight.class, ExecutorService.class), + Qualified.qualified(Lightweight.class, Executor.class)) + .factory(c -> LITE_EXECUTOR.get()) + .build(), + Component.builder(Qualified.qualified(UiThread.class, Executor.class)) + .factory(c -> UiExecutor.INSTANCE) + .build()); + } + + private static ScheduledExecutorService scheduled(ExecutorService delegate) { + return new DelegatingScheduledExecutorService(delegate, SCHEDULER.get()); + } + + private static ThreadFactory factory(String threadPrefix, int priority) { + return new CustomThreadFactory(threadPrefix, priority, null); + } + + private static ThreadFactory factory( + String threadPrefix, int priority, StrictMode.ThreadPolicy policy) { + return new CustomThreadFactory(threadPrefix, priority, policy); + } + + private static StrictMode.ThreadPolicy bgPolicy() { + StrictMode.ThreadPolicy.Builder builder = new StrictMode.ThreadPolicy.Builder().detectNetwork(); + if (Build.VERSION.SDK_INT >= 23) { + builder.detectResourceMismatches(); + if (Build.VERSION.SDK_INT >= 26) { + builder.detectUnbufferedIo(); + } + } + if (BuildConfig.DEBUG) { + builder.penaltyDeath(); + } + return builder.penaltyLog().build(); + } + + private static StrictMode.ThreadPolicy litePolicy() { + StrictMode.ThreadPolicy.Builder builder = new StrictMode.ThreadPolicy.Builder().detectAll(); + if (BuildConfig.DEBUG) { + builder.penaltyDeath(); + } + return builder.penaltyLog().build(); + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/FirebaseExecutors.java b/firebase-common/src/main/java/com/google/firebase/concurrent/FirebaseExecutors.java new file mode 100644 index 00000000000..a31ff759fa6 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/FirebaseExecutors.java @@ -0,0 +1,49 @@ +// Copyright 2022 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.concurrent; + +import java.util.concurrent.Executor; + +/** Provides commonly useful executors. */ +public class FirebaseExecutors { + private FirebaseExecutors() {} + + /** + * Creates a sequential executor. + * + *

Executes tasks sequentially and provides memory synchronization guarantees for any mutations + * of shared state. + * + *

For details see: + * https://guava.dev/releases/31.1-jre/api/docs/com/google/common/util/concurrent/MoreExecutors.html#newSequentialExecutor(java.util.concurrent.Executor) + */ + public static Executor newSequentialExecutor(Executor delegate) { + return new SequentialExecutor(delegate); + } + + /** Returns a direct executor. */ + public static Executor directExecutor() { + return DirectExecutor.INSTANCE; + } + + private enum DirectExecutor implements Executor { + INSTANCE; + + @Override + public void execute(Runnable command) { + command.run(); + } + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/concurrent/SequentialExecutor.java b/firebase-common/src/main/java/com/google/firebase/concurrent/SequentialExecutor.java new file mode 100644 index 00000000000..db5f4f3a374 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/SequentialExecutor.java @@ -0,0 +1,261 @@ +// Copyright 2022 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.concurrent; + +import static com.google.android.gms.common.internal.Preconditions.checkNotNull; +import static com.google.firebase.concurrent.SequentialExecutor.WorkerRunningState.IDLE; +import static com.google.firebase.concurrent.SequentialExecutor.WorkerRunningState.QUEUED; +import static com.google.firebase.concurrent.SequentialExecutor.WorkerRunningState.QUEUING; +import static com.google.firebase.concurrent.SequentialExecutor.WorkerRunningState.RUNNING; +import static java.lang.System.identityHashCode; + +import androidx.annotation.GuardedBy; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; + +/** + * Executor ensuring that all Runnables submitted are executed in order, using the provided + * Executor, and sequentially such that no two will ever be running at the same time. + * + *

Tasks submitted to {@link #execute(Runnable)} are executed in FIFO order. + * + *

The execution of tasks is done by one thread as long as there are tasks left in the queue. + * When a task is {@linkplain Thread#interrupt interrupted}, execution of subsequent tasks + * continues. See {@link QueueWorker#workOnQueue} for details. + * + *

{@code RuntimeException}s thrown by tasks are simply logged and the executor keeps trucking. + * If an {@code Error} is thrown, the error will propagate and execution will stop until it is + * restarted by a call to {@link #execute}. + */ +final class SequentialExecutor implements Executor { + private static final Logger log = Logger.getLogger(SequentialExecutor.class.getName()); + + enum WorkerRunningState { + /** Runnable is not running and not queued for execution */ + IDLE, + /** Runnable is not running, but is being queued for execution */ + QUEUING, + /** runnable has been submitted but has not yet begun execution */ + QUEUED, + RUNNING, + } + + /** Underlying executor that all submitted Runnable objects are run on. */ + private final Executor executor; + + @GuardedBy("queue") + private final Deque queue = new ArrayDeque<>(); + + /** see {@link WorkerRunningState} */ + @GuardedBy("queue") + private WorkerRunningState workerRunningState = IDLE; + + /** + * This counter prevents an ABA issue where a thread may successfully schedule the worker, the + * worker runs and exhausts the queue, another thread enqueues a task and fails to schedule the + * worker, and then the first thread's call to delegate.execute() returns. Without this counter, + * it would observe the QUEUING state and set it to QUEUED, and the worker would never be + * scheduled again for future submissions. + */ + @GuardedBy("queue") + private long workerRunCount = 0; + + private final QueueWorker worker = new QueueWorker(); + + SequentialExecutor(Executor executor) { + this.executor = checkNotNull(executor); + } + + /** + * Adds a task to the queue and makes sure a worker thread is running. + * + *

If this method throws, e.g. a {@code RejectedExecutionException} from the delegate executor, + * execution of tasks will stop until a call to this method is made. + */ + @Override + public void execute(Runnable task) { + checkNotNull(task); + Runnable submittedTask; + long oldRunCount; + synchronized (queue) { + // If the worker is already running (or execute() on the delegate returned successfully, and + // the worker has yet to start) then we don't need to start the worker. + if (workerRunningState == RUNNING || workerRunningState == QUEUED) { + queue.add(task); + return; + } + + oldRunCount = workerRunCount; + + // If the worker is not yet running, the delegate Executor might reject our attempt to start + // it. To preserve FIFO order and failure atomicity of rejected execution when the same + // Runnable is executed more than once, allocate a wrapper that we know is safe to remove by + // object identity. + // A data structure that returned a removal handle from add() would allow eliminating this + // allocation. + submittedTask = + new Runnable() { + @Override + public void run() { + task.run(); + } + + @Override + public String toString() { + return task.toString(); + } + }; + queue.add(submittedTask); + workerRunningState = QUEUING; + } + + try { + executor.execute(worker); + } catch (RuntimeException | Error t) { + synchronized (queue) { + boolean removed = + (workerRunningState == IDLE || workerRunningState == QUEUING) + && queue.removeLastOccurrence(submittedTask); + // If the delegate is directExecutor(), the submitted runnable could have thrown a REE. But + // that's handled by the log check that catches RuntimeExceptions in the queue worker. + if (!(t instanceof RejectedExecutionException) || removed) { + throw t; + } + } + return; + } + + /* + * This is an unsynchronized read! After the read, the function returns immediately or acquires + * the lock to check again. Since an IDLE state was observed inside the preceding synchronized + * block, and reference field assignment is atomic, this may save reacquiring the lock when + * another thread or the worker task has cleared the count and set the state. + * + *

When {@link #executor} is a directExecutor(), the value written to + * {@code workerRunningState} will be available synchronously, and behaviour will be + * deterministic. + */ + @SuppressWarnings("GuardedBy") + boolean alreadyMarkedQueued = workerRunningState != QUEUING; + if (alreadyMarkedQueued) { + return; + } + synchronized (queue) { + if (workerRunCount == oldRunCount && workerRunningState == QUEUING) { + workerRunningState = QUEUED; + } + } + } + + /** Worker that runs tasks from {@link #queue} until it is empty. */ + private final class QueueWorker implements Runnable { + @CheckForNull Runnable task; + + @Override + public void run() { + try { + workOnQueue(); + } catch (Error e) { + synchronized (queue) { + workerRunningState = IDLE; + } + throw e; + // The execution of a task has ended abnormally. + // We could have tasks left in the queue, so should perhaps try to restart a worker, + // but then the Error will get delayed if we are using a direct (same thread) executor. + } + } + + /** + * Continues executing tasks from {@link #queue} until it is empty. + * + *

The thread's interrupt bit is cleared before execution of each task. + * + *

If the Thread in use is interrupted before or during execution of the tasks in {@link + * #queue}, the Executor will complete its tasks, and then restore the interruption. This means + * that once the Thread returns to the Executor that this Executor composes, the interruption + * will still be present. If the composed Executor is an ExecutorService, it can respond to + * shutdown() by returning tasks queued on that Thread after {@link #worker} drains the queue. + */ + private void workOnQueue() { + boolean interruptedDuringTask = false; + boolean hasSetRunning = false; + try { + while (true) { + synchronized (queue) { + // Choose whether this thread will run or not after acquiring the lock on the first + // iteration + if (!hasSetRunning) { + if (workerRunningState == RUNNING) { + // Don't want to have two workers pulling from the queue. + return; + } else { + // Increment the run counter to avoid the ABA problem of a submitter marking the + // thread as QUEUED after it already ran and exhausted the queue before returning + // from execute(). + workerRunCount++; + workerRunningState = RUNNING; + hasSetRunning = true; + } + } + task = queue.poll(); + if (task == null) { + workerRunningState = IDLE; + return; + } + } + // Remove the interrupt bit before each task. The interrupt is for the "current task" when + // it is sent, so subsequent tasks in the queue should not be caused to be interrupted + // by a previous one in the queue being interrupted. + interruptedDuringTask |= Thread.interrupted(); + try { + task.run(); + } catch (RuntimeException e) { + log.log(Level.SEVERE, "Exception while executing runnable " + task, e); + } finally { + task = null; + } + } + } finally { + // Ensure that if the thread was interrupted at all while processing the task queue, it + // is returned to the delegate Executor interrupted so that it may handle the + // interruption if it likes. + if (interruptedDuringTask) { + Thread.currentThread().interrupt(); + } + } + } + + @SuppressWarnings("GuardedBy") + @Override + public String toString() { + Runnable currentlyRunning = task; + if (currentlyRunning != null) { + return "SequentialExecutorWorker{running=" + currentlyRunning + "}"; + } + return "SequentialExecutorWorker{state=" + workerRunningState + "}"; + } + } + + @Override + public String toString() { + return "SequentialExecutor@" + identityHashCode(this) + "{" + executor + "}"; + } +} diff --git a/firebase-components/src/main/java/com/google/firebase/components/AbstractComponentContainer.java b/firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java similarity index 52% rename from firebase-components/src/main/java/com/google/firebase/components/AbstractComponentContainer.java rename to firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java index 143adf0acc7..5352e1f1f8a 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/AbstractComponentContainer.java +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/UiExecutor.java @@ -1,4 +1,4 @@ -// Copyright 2018 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,23 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.components; +package com.google.firebase.concurrent; -import com.google.firebase.inject.Provider; -import java.util.Set; +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Executor; -abstract class AbstractComponentContainer implements ComponentContainer { - @Override - public T get(Class anInterface) { - Provider provider = getProvider(anInterface); - if (provider == null) { - return null; - } - return provider.get(); - } +/** @hide */ +public enum UiExecutor implements Executor { + INSTANCE; + + // This is the only UI handler that is allowed in Firebase SDK. + @SuppressLint("ThreadPoolCreation") + private static final Handler HANDLER = new Handler(Looper.getMainLooper()); @Override - public Set setOf(Class anInterface) { - return setOfProvider(anInterface).get(); + public void execute(Runnable command) { + HANDLER.post(command); } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/provider/package-info.java b/firebase-common/src/main/java/com/google/firebase/concurrent/package-info.java similarity index 88% rename from firebase-perf/src/main/java/com/google/firebase/perf/provider/package-info.java rename to firebase-common/src/main/java/com/google/firebase/concurrent/package-info.java index d3aff47a348..9452eb485fd 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/provider/package-info.java +++ b/firebase-common/src/main/java/com/google/firebase/concurrent/package-info.java @@ -1,9 +1,9 @@ -// Copyright 2020 Google LLC +// 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 @@ -13,4 +13,4 @@ // limitations under the License. /** @hide */ -package com.google.firebase.perf.provider; +package com.google.firebase.concurrent; diff --git a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java index cb7722c37bd..ff9ba5123fc 100644 --- a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java +++ b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java @@ -23,18 +23,16 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.inject.Provider; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.ByteArrayOutputStream; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.json.JSONArray; import org.json.JSONObject; @@ -52,9 +50,6 @@ public class DefaultHeartBeatController implements HeartBeatController, HeartBea private final Executor backgroundExecutor; - private static final ThreadFactory THREAD_FACTORY = - r -> new Thread(r, "heartbeat-information-executor"); - public Task registerHeartBeat() { if (consumers.size() <= 0) { return Tasks.forResult(null); @@ -118,12 +113,12 @@ private DefaultHeartBeatController( Context context, String persistenceKey, Set consumers, - Provider userAgentProvider) { + Provider userAgentProvider, + Executor backgroundExecutor) { this( () -> new HeartBeatInfoStorage(context, persistenceKey), consumers, - new ThreadPoolExecutor( - 0, 1, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), THREAD_FACTORY), + backgroundExecutor, userAgentProvider, context); } @@ -143,19 +138,22 @@ private DefaultHeartBeatController( } public static @NonNull Component component() { + Qualified backgroundExecutor = Qualified.qualified(Background.class, Executor.class); return Component.builder( DefaultHeartBeatController.class, HeartBeatController.class, HeartBeatInfo.class) .add(Dependency.required(Context.class)) .add(Dependency.required(FirebaseApp.class)) .add(Dependency.setOf(HeartBeatConsumer.class)) .add(Dependency.requiredProvider(UserAgentPublisher.class)) + .add(Dependency.required(backgroundExecutor)) .factory( c -> new DefaultHeartBeatController( c.get(Context.class), c.get(FirebaseApp.class).getPersistenceKey(), c.setOf(HeartBeatConsumer.class), - c.getProvider(UserAgentPublisher.class))) + c.getProvider(UserAgentPublisher.class), + c.get(backgroundExecutor))) .build(); } diff --git a/firebase-common/src/main/java/com/google/firebase/provider/FirebaseInitProvider.java b/firebase-common/src/main/java/com/google/firebase/provider/FirebaseInitProvider.java index e4db850fa63..a43c884ed1d 100644 --- a/firebase-common/src/main/java/com/google/firebase/provider/FirebaseInitProvider.java +++ b/firebase-common/src/main/java/com/google/firebase/provider/FirebaseInitProvider.java @@ -27,11 +27,24 @@ import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Preconditions; import com.google.firebase.FirebaseApp; +import com.google.firebase.StartupTime; +import java.util.concurrent.atomic.AtomicBoolean; /** Initializes Firebase APIs at app startup time. */ public class FirebaseInitProvider extends ContentProvider { - private static final String TAG = "FirebaseInitProvider"; + @Nullable private static StartupTime startupTime = StartupTime.now(); + @NonNull private static AtomicBoolean currentlyInitializing = new AtomicBoolean(false); + + /** @hide */ + public static @Nullable StartupTime getStartupTime() { + return startupTime; + } + + /** @hide */ + public static boolean isCurrentlyInitializing() { + return currentlyInitializing.get(); + } /** Should match the {@link FirebaseInitProvider} authority if $androidId is empty. */ @VisibleForTesting @@ -48,12 +61,17 @@ public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { /** Called before {@link Application#onCreate()}. */ @Override public boolean onCreate() { - if (FirebaseApp.initializeApp(getContext()) == null) { - Log.i(TAG, "FirebaseApp initialization unsuccessful"); - } else { - Log.i(TAG, "FirebaseApp initialization successful"); + try { + currentlyInitializing.set(true); + if (FirebaseApp.initializeApp(getContext()) == null) { + Log.i(TAG, "FirebaseApp initialization unsuccessful"); + } else { + Log.i(TAG, "FirebaseApp initialization successful"); + } + return false; + } finally { + currentlyInitializing.set(false); } - return false; } /** diff --git a/firebase-common/src/test/AndroidManifest.xml b/firebase-common/src/test/AndroidManifest.xml new file mode 100644 index 00000000000..b0baac40e37 --- /dev/null +++ b/firebase-common/src/test/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java b/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java new file mode 100644 index 00000000000..0717ddf4ee8 --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/DelegatingScheduledExecutorServiceTest.java @@ -0,0 +1,82 @@ +// Copyright 2022 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.concurrent; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class DelegatingScheduledExecutorServiceTest { + private final DelegatingScheduledExecutorService service = + new DelegatingScheduledExecutorService( + Executors.newCachedThreadPool(), Executors.newSingleThreadScheduledExecutor()); + + @Test + public void schedule_whenCancelled_shouldCancelUnderlyingFuture() { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean ran = new AtomicBoolean(); + ScheduledFuture future = + service.schedule( + () -> { + latch.await(); + ran.set(true); + return null; + }, + 10, + TimeUnit.SECONDS); + future.cancel(true); + latch.countDown(); + assertThat(ran.get()).isFalse(); + } + + @Test + public void scheduleAtFixedRate_whenRunnableThrows_shouldCancelSchedule() + throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + AtomicLong ran = new AtomicLong(); + + ScheduledFuture future = + service.scheduleAtFixedRate( + () -> { + ran.incrementAndGet(); + throw new RuntimeException("fail"); + }, + 1, + 1, + TimeUnit.SECONDS); + + semaphore.release(); + try { + future.get(); + fail("Expected exception not thrown"); + } catch (ExecutionException ex) { + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + assertThat(ex.getCause().getMessage()).isEqualTo("fail"); + } + assertThat(ran.get()).isEqualTo(1); + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java new file mode 100644 index 00000000000..0da8650a80f --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponent.java @@ -0,0 +1,58 @@ +// Copyright 2022 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.concurrent; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +class ExecutorComponent { + final ScheduledExecutorService bgScheduledService; + final ExecutorService bgService; + final Executor bgExecutor; + + final ScheduledExecutorService liteScheduledService; + final ExecutorService liteService; + final Executor liteExecutor; + + final ScheduledExecutorService blockingScheduledService; + final ExecutorService blockingService; + final Executor blockingExecutor; + + final Executor uiExecutor; + + public ExecutorComponent( + ScheduledExecutorService bgScheduledService, + ExecutorService bgService, + Executor bgExecutor, + ScheduledExecutorService liteScheduledService, + ExecutorService liteService, + Executor liteExecutor, + ScheduledExecutorService blockingScheduledService, + ExecutorService blockingService, + Executor blockingExecutor, + Executor uiExecutor) { + this.bgScheduledService = bgScheduledService; + this.bgService = bgService; + this.bgExecutor = bgExecutor; + this.liteScheduledService = liteScheduledService; + this.liteService = liteService; + this.liteExecutor = liteExecutor; + this.blockingScheduledService = blockingScheduledService; + this.blockingService = blockingService; + this.blockingExecutor = blockingExecutor; + this.uiExecutor = uiExecutor; + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java new file mode 100644 index 00000000000..bf560c88e95 --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorComponentTest.java @@ -0,0 +1,46 @@ +// Copyright 2022 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.concurrent; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.FirebaseAppTestUtil.withApp; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.firebase.FirebaseOptions; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ExecutorComponentTest { + private static final FirebaseOptions OPTIONS = + new FirebaseOptions.Builder() + .setApiKey("myKey") + .setApplicationId("123") + .setProjectId("456") + .build(); + + @Test + public void testThatAllExecutorsAreRegisteredByCommon() { + withApp( + "test", + OPTIONS, + app -> { + ExecutorComponent executorComponent = app.get(ExecutorComponent.class); + // If the component is not null, it means it was able to get all of its required + // dependencies, otherwise get() would throw. + assertThat(executorComponent).isNotNull(); + }); + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java new file mode 100644 index 00000000000..9fdcf7d428f --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/concurrent/ExecutorTestsRegistrar.java @@ -0,0 +1,81 @@ +// Copyright 2022 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.concurrent; + +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.components.Component; +import com.google.firebase.components.ComponentRegistrar; +import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +public class ExecutorTestsRegistrar implements ComponentRegistrar { + @Override + public List> getComponents() { + Qualified bgScheduledService = + Qualified.qualified(Background.class, ScheduledExecutorService.class); + Qualified bgService = + Qualified.qualified(Background.class, ExecutorService.class); + Qualified bgExecutor = Qualified.qualified(Background.class, Executor.class); + + Qualified liteScheduledService = + Qualified.qualified(Lightweight.class, ScheduledExecutorService.class); + Qualified liteService = + Qualified.qualified(Lightweight.class, ExecutorService.class); + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + + Qualified blockingScheduledService = + Qualified.qualified(Blocking.class, ScheduledExecutorService.class); + Qualified blockingService = + Qualified.qualified(Blocking.class, ExecutorService.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); + + return Collections.singletonList( + Component.builder(ExecutorComponent.class) + .add(Dependency.required(bgScheduledService)) + .add(Dependency.required(bgService)) + .add(Dependency.required(bgExecutor)) + .add(Dependency.required(liteScheduledService)) + .add(Dependency.required(liteService)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(blockingScheduledService)) + .add(Dependency.required(blockingService)) + .add(Dependency.required(blockingExecutor)) + .add(Dependency.required(uiExecutor)) + .factory( + c -> + new ExecutorComponent( + c.get(bgScheduledService), + c.get(bgService), + c.get(bgExecutor), + c.get(liteScheduledService), + c.get(liteService), + c.get(liteExecutor), + c.get(blockingScheduledService), + c.get(blockingService), + c.get(blockingExecutor), + c.get(uiExecutor))) + .build()); + } +} diff --git a/firebase-components/firebase-components.gradle b/firebase-components/firebase-components.gradle deleted file mode 100644 index 9c48904de3b..00000000000 --- a/firebase-components/firebase-components.gradle +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' -} - -firebaseLibrary { - publishSources = true - publishJavadoc = false -} - -android { - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - versionName version - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard.txt' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - implementation project(':firebase-annotations') - implementation 'androidx.annotation:annotation:1.1.0' - implementation "com.google.errorprone:error_prone_annotations:2.9.0" - testImplementation 'androidx.test:runner:1.2.0' - testImplementation 'androidx.test.ext:junit:1.1.1' - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.13' - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'org.mockito:mockito-core:2.25.0' - -} diff --git a/firebase-components/firebase-components.gradle.kts b/firebase-components/firebase-components.gradle.kts new file mode 100644 index 00000000000..6ce4038a544 --- /dev/null +++ b/firebase-components/firebase-components.gradle.kts @@ -0,0 +1,53 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +plugins { + id("firebase-library") +} + +firebaseLibrary { + publishSources = true + publishJavadoc = false +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + compileSdk = targetSdkVersion + defaultConfig { + minSdk = minSdkVersion + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("proguard.txt") + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-annotations")) + implementation(libs.androidx.annotation) + implementation(libs.errorprone.annotations) + + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.robolectric) + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.mockito.core) +} diff --git a/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle b/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle deleted file mode 100644 index 78ecbfd7219..00000000000 --- a/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' -} - -group = 'com.google.firebase' - -firebaseLibrary { - testLab.enabled = false - publishSources = true - publishJavadoc = false -} - -android { - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - targetSdkVersion project.targetSdkVersion - versionName version - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests { - includeAndroidResources = true - } - } -} - -dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation 'com.google.android.play:feature-delivery:2.0.0' -} diff --git a/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle.kts b/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle.kts new file mode 100644 index 00000000000..7bf75888b39 --- /dev/null +++ b/firebase-components/firebase-dynamic-module-support/firebase-dynamic-module-support.gradle.kts @@ -0,0 +1,48 @@ +// Copyright 2022 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") +} + +group = "com.google.firebase" + +firebaseLibrary { + testLab.enabled = false + publishSources = true + publishJavadoc = false +} + +android { + val targetSdkVersion : Int by rootProject + val minSdkVersion : Int by rootProject + compileSdk = targetSdkVersion + defaultConfig { + minSdk = minSdkVersion + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) + implementation("com.google.android.play:feature-delivery:2.0.0") +} diff --git a/firebase-components/src/main/java/com/google/firebase/components/Component.java b/firebase-components/src/main/java/com/google/firebase/components/Component.java index 3e2590fde09..768baa48568 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/Component.java +++ b/firebase-components/src/main/java/com/google/firebase/components/Component.java @@ -80,7 +80,7 @@ public final class Component { } private final String name; - private final Set> providedInterfaces; + private final Set> providedInterfaces; private final Set dependencies; private final @Instantiation int instantiation; private final @ComponentType int type; @@ -89,7 +89,7 @@ public final class Component { private Component( @Nullable String name, - Set> providedInterfaces, + Set> providedInterfaces, Set dependencies, @Instantiation int instantiation, @ComponentType int type, @@ -119,7 +119,7 @@ public String getName() { * *

Note: T conforms to all of these interfaces. */ - public Set> getProvidedInterfaces() { + public Set> getProvidedInterfaces() { return providedInterfaces; } @@ -202,6 +202,18 @@ public static Component.Builder builder( return new Builder<>(anInterface, additionalInterfaces); } + /** Returns a Component builder. */ + public static Component.Builder builder(Qualified anInterface) { + return new Builder<>(anInterface); + } + + /** Returns a Component builder. */ + @SafeVarargs + public static Component.Builder builder( + Qualified anInterface, Qualified... additionalInterfaces) { + return new Builder<>(anInterface, additionalInterfaces); + } + /** * Wraps a value in a {@link Component} with no dependencies. * @@ -219,6 +231,13 @@ public static Component of( return builder(anInterface, additionalInterfaces).factory((args) -> value).build(); } + /** Wraps a value in a {@link Component} with no dependencies. */ + @SafeVarargs + public static Component of( + T value, Qualified anInterface, Qualified... additionalInterfaces) { + return builder(anInterface, additionalInterfaces).factory((args) -> value).build(); + } + /** * Provides a builder for a {@link Set}-multibinding {@link Component}. * @@ -229,6 +248,16 @@ public static Component.Builder intoSetBuilder(Class anInterface) { return builder(anInterface).intoSet(); } + /** + * Provides a builder for a {@link Set}-multibinding {@link Component}. + * + *

Such components can be requested by dependents via {@link ComponentContainer#setOf(Class)} * + * or {@link ComponentContainer#setOfProvider(Class)}. + */ + public static Component.Builder intoSetBuilder(Qualified anInterface) { + return builder(anInterface).intoSet(); + } + /** * Wraps a value in a {@link Set}-multibinding {@link Component} with no dependencies. * * @@ -239,22 +268,42 @@ public static Component intoSet(T value, Class anInterface) { return intoSetBuilder(anInterface).factory(c -> value).build(); } + /** + * Wraps a value in a {@link Set}-multibinding {@link Component} with no dependencies. * + * + *

Such components can be requested by dependents via {@link ComponentContainer#setOf(Class)} * + * or {@link ComponentContainer#setOfProvider(Class)}. + */ + public static Component intoSet(T value, Qualified anInterface) { + return intoSetBuilder(anInterface).factory(c -> value).build(); + } + /** FirebaseComponent builder. */ public static class Builder { private String name = null; - private final Set> providedInterfaces = new HashSet<>(); + private final Set> providedInterfaces = new HashSet<>(); private final Set dependencies = new HashSet<>(); private @Instantiation int instantiation = Instantiation.LAZY; private @ComponentType int type = ComponentType.VALUE; private ComponentFactory factory; - private Set> publishedEvents = new HashSet<>(); + private final Set> publishedEvents = new HashSet<>(); @SafeVarargs private Builder(Class anInterface, Class... additionalInterfaces) { Preconditions.checkNotNull(anInterface, "Null interface"); - providedInterfaces.add(anInterface); + providedInterfaces.add(Qualified.unqualified(anInterface)); for (Class iface : additionalInterfaces) { Preconditions.checkNotNull(iface, "Null interface"); + providedInterfaces.add(Qualified.unqualified(iface)); + } + } + + @SafeVarargs + private Builder(Qualified anInterface, Qualified... additionalInterfaces) { + Preconditions.checkNotNull(anInterface, "Null interface"); + providedInterfaces.add(anInterface); + for (Qualified iface : additionalInterfaces) { + Preconditions.checkNotNull(iface, "Null interface"); } Collections.addAll(providedInterfaces, additionalInterfaces); } @@ -301,7 +350,7 @@ private Builder setInstantiation(@Instantiation int instantiation) { return this; } - private void validateInterface(Class anInterface) { + private void validateInterface(Qualified anInterface) { Preconditions.checkArgument( !providedInterfaces.contains(anInterface), "Components are not allowed to depend on interfaces they themselves provide."); diff --git a/firebase-components/src/main/java/com/google/firebase/components/ComponentContainer.java b/firebase-components/src/main/java/com/google/firebase/components/ComponentContainer.java index d5c349eea9c..0f301eb0e19 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/ComponentContainer.java +++ b/firebase-components/src/main/java/com/google/firebase/components/ComponentContainer.java @@ -20,13 +20,41 @@ /** Provides a means to retrieve instances of requested classes/interfaces. */ public interface ComponentContainer { - T get(Class anInterface); + default T get(Class anInterface) { + return get(Qualified.unqualified(anInterface)); + } - Provider getProvider(Class anInterface); + default Provider getProvider(Class anInterface) { + return getProvider(Qualified.unqualified(anInterface)); + } - Deferred getDeferred(Class anInterface); + default Deferred getDeferred(Class anInterface) { + return getDeferred(Qualified.unqualified(anInterface)); + } - Set setOf(Class anInterface); + default Set setOf(Class anInterface) { + return setOf(Qualified.unqualified(anInterface)); + } - Provider> setOfProvider(Class anInterface); + default Provider> setOfProvider(Class anInterface) { + return setOfProvider(Qualified.unqualified(anInterface)); + } + + default T get(Qualified anInterface) { + Provider provider = getProvider(anInterface); + if (provider == null) { + return null; + } + return provider.get(); + } + + Provider getProvider(Qualified anInterface); + + Deferred getDeferred(Qualified anInterface); + + default Set setOf(Qualified anInterface) { + return setOfProvider(anInterface).get(); + } + + Provider> setOfProvider(Qualified anInterface); } diff --git a/firebase-components/src/main/java/com/google/firebase/components/ComponentRuntime.java b/firebase-components/src/main/java/com/google/firebase/components/ComponentRuntime.java index 895b19fff15..a560cf5b502 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/ComponentRuntime.java +++ b/firebase-components/src/main/java/com/google/firebase/components/ComponentRuntime.java @@ -43,11 +43,11 @@ *

Does {@link Component} dependency resolution and provides access to resolved {@link * Component}s via {@link #get(Class)} method. */ -public class ComponentRuntime extends AbstractComponentContainer implements ComponentLoader { +public class ComponentRuntime implements ComponentContainer, ComponentLoader { private static final Provider> EMPTY_PROVIDER = Collections::emptySet; private final Map, Provider> components = new HashMap<>(); - private final Map, Provider> lazyInstanceMap = new HashMap<>(); - private final Map, LazySet> lazySetMap = new HashMap<>(); + private final Map, Provider> lazyInstanceMap = new HashMap<>(); + private final Map, LazySet> lazySetMap = new HashMap<>(); private final List> unprocessedRegistrarProviders; private final EventBus eventBus; private final AtomicReference eagerComponentsInitializedWith = new AtomicReference<>(); @@ -184,7 +184,7 @@ private List processInstanceComponents(List> componentsTo } Provider provider = components.get(component); - for (Class anInterface : component.getProvidedInterfaces()) { + for (Qualified anInterface : component.getProvidedInterfaces()) { if (!lazyInstanceMap.containsKey(anInterface)) { lazyInstanceMap.put(anInterface, provider); } else { @@ -203,7 +203,7 @@ private List processInstanceComponents(List> componentsTo /** Populates lazySetMap to make set components available for consumption via set dependencies. */ private List processSetComponents() { ArrayList runnables = new ArrayList<>(); - Map, Set>> setIndex = new HashMap<>(); + Map, Set>> setIndex = new HashMap<>(); for (Map.Entry, Provider> entry : components.entrySet()) { Component component = entry.getKey(); @@ -214,7 +214,7 @@ private List processSetComponents() { Provider provider = entry.getValue(); - for (Class anInterface : component.getProvidedInterfaces()) { + for (Qualified anInterface : component.getProvidedInterfaces()) { if (!setIndex.containsKey(anInterface)) { setIndex.put(anInterface, new HashSet<>()); } @@ -222,7 +222,7 @@ private List processSetComponents() { } } - for (Map.Entry, Set>> entry : setIndex.entrySet()) { + for (Map.Entry, Set>> entry : setIndex.entrySet()) { if (!lazySetMap.containsKey(entry.getKey())) { lazySetMap.put(entry.getKey(), LazySet.fromCollection(entry.getValue())); } else { @@ -240,13 +240,13 @@ private List processSetComponents() { @Override @SuppressWarnings("unchecked") - public synchronized Provider getProvider(Class anInterface) { + public synchronized Provider getProvider(Qualified anInterface) { Preconditions.checkNotNull(anInterface, "Null interface requested."); return (Provider) lazyInstanceMap.get(anInterface); } @Override - public Deferred getDeferred(Class anInterface) { + public Deferred getDeferred(Qualified anInterface) { Provider provider = getProvider(anInterface); if (provider == null) { return OptionalProvider.empty(); @@ -259,7 +259,7 @@ public Deferred getDeferred(Class anInterface) { @Override @SuppressWarnings("unchecked") - public synchronized Provider> setOfProvider(Class anInterface) { + public synchronized Provider> setOfProvider(Qualified anInterface) { LazySet provider = lazySetMap.get(anInterface); if (provider != null) { return (Provider>) (Provider) provider; diff --git a/firebase-components/src/main/java/com/google/firebase/components/CycleDetector.java b/firebase-components/src/main/java/com/google/firebase/components/CycleDetector.java index a435d6f3a8c..38d7124c11c 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/CycleDetector.java +++ b/firebase-components/src/main/java/com/google/firebase/components/CycleDetector.java @@ -24,10 +24,10 @@ /** Cycle detector for the {@link Component} dependency graph. */ class CycleDetector { private static class Dep { - private final Class anInterface; + private final Qualified anInterface; private final boolean set; - private Dep(Class anInterface, boolean set) { + private Dep(Qualified anInterface, boolean set) { this.anInterface = anInterface; this.set = set; } @@ -135,7 +135,7 @@ private static Set toGraph(List> components) { Map> componentIndex = new HashMap<>(components.size()); for (Component component : components) { ComponentNode node = new ComponentNode(component); - for (Class anInterface : component.getProvidedInterfaces()) { + for (Qualified anInterface : component.getProvidedInterfaces()) { Dep cmp = new Dep(anInterface, !component.isValue()); if (!componentIndex.containsKey(cmp)) { componentIndex.put(cmp, new HashSet<>()); diff --git a/firebase-components/src/main/java/com/google/firebase/components/Dependency.java b/firebase-components/src/main/java/com/google/firebase/components/Dependency.java index ed8a3c3b66d..9c9ef676a75 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/Dependency.java +++ b/firebase-components/src/main/java/com/google/firebase/components/Dependency.java @@ -37,11 +37,15 @@ public final class Dependency { int DEFERRED = 2; } - private final Class anInterface; - private final @Type int type; + private final Qualified anInterface; + @Type private final int type; private final @Injection int injection; private Dependency(Class anInterface, @Type int type, @Injection int injection) { + this(Qualified.unqualified(anInterface), type, injection); + } + + private Dependency(Qualified anInterface, @Type int type, @Injection int injection) { this.anInterface = Preconditions.checkNotNull(anInterface, "Null dependency anInterface."); this.type = type; this.injection = injection; @@ -71,6 +75,16 @@ public static Dependency deferred(Class anInterface) { return new Dependency(anInterface, Type.OPTIONAL, Injection.DEFERRED); } + /** + * Declares a deferred dependency. + * + *

Such dependencies are optional and may not be present by default. But they can become + * available if a dynamic module that contains them is installed. + */ + public static Dependency deferred(Qualified anInterface) { + return new Dependency(anInterface, Type.OPTIONAL, Injection.DEFERRED); + } + /** * Declares a required dependency. * @@ -83,6 +97,18 @@ public static Dependency required(Class anInterface) { return new Dependency(anInterface, Type.REQUIRED, Injection.DIRECT); } + /** + * Declares a required dependency. + * + *

Such dependencies must be present in order for the dependent component to function. Any + * component with a required dependency should also declare a Maven dependency on an SDK that + * provides it. Failing to do so will result in a {@link MissingDependencyException} to be thrown + * at runtime. + */ + public static Dependency required(Qualified anInterface) { + return new Dependency(anInterface, Type.REQUIRED, Injection.DIRECT); + } + /** * Declares a Set multi-binding dependency. * @@ -94,6 +120,17 @@ public static Dependency setOf(Class anInterface) { return new Dependency(anInterface, Type.SET, Injection.DIRECT); } + /** + * Declares a Set multi-binding dependency. + * + *

Such dependencies provide access to a {@code Set} to dependent components. Note that + * the set is only filled with components that explicitly declare the intent to be a "set" + * dependency via {@link Component#intoSet(Object, Class)}. + */ + public static Dependency setOf(Qualified anInterface) { + return new Dependency(anInterface, Type.SET, Injection.DIRECT); + } + /** * Declares an optional dependency. * @@ -104,6 +141,16 @@ public static Dependency optionalProvider(Class anInterface) { return new Dependency(anInterface, Type.OPTIONAL, Injection.PROVIDER); } + /** + * Declares an optional dependency. + * + *

Optional dependencies can be missing at runtime(being {@code null}) and dependents must be + * ready to handle that. + */ + public static Dependency optionalProvider(Qualified anInterface) { + return new Dependency(anInterface, Type.OPTIONAL, Injection.PROVIDER); + } + /** * Declares a required dependency. * @@ -116,6 +163,18 @@ public static Dependency requiredProvider(Class anInterface) { return new Dependency(anInterface, Type.REQUIRED, Injection.PROVIDER); } + /** + * Declares a required dependency. + * + *

Such dependencies must be present in order for the dependent component to function. Any + * component with a required dependency should also declare a Maven dependency on an SDK that + * provides it. Failing to do so will result in a {@link MissingDependencyException} to be thrown + * at runtime. + */ + public static Dependency requiredProvider(Qualified anInterface) { + return new Dependency(anInterface, Type.REQUIRED, Injection.PROVIDER); + } + /** * Declares a Set multi-binding dependency. * @@ -127,7 +186,18 @@ public static Dependency setOfProvider(Class anInterface) { return new Dependency(anInterface, Type.SET, Injection.PROVIDER); } - public Class getInterface() { + /** + * Declares a Set multi-binding dependency. + * + *

Such dependencies provide access to a {@code Set} to dependent components. Note that + * the set is only filled with components that explicitly declare the intent to be a "set" + * dependency via {@link Component#intoSet(Object, Class)}. + */ + public static Dependency setOfProvider(Qualified anInterface) { + return new Dependency(anInterface, Type.SET, Injection.PROVIDER); + } + + public Qualified getInterface() { return anInterface; } @@ -151,7 +221,9 @@ public boolean isDeferred() { public boolean equals(Object o) { if (o instanceof Dependency) { Dependency other = (Dependency) o; - return anInterface == other.anInterface && type == other.type && injection == other.injection; + return anInterface.equals(other.anInterface) + && type == other.type + && injection == other.injection; } return false; } diff --git a/firebase-components/src/main/java/com/google/firebase/components/Qualified.java b/firebase-components/src/main/java/com/google/firebase/components/Qualified.java new file mode 100644 index 00000000000..a0b755ac583 --- /dev/null +++ b/firebase-components/src/main/java/com/google/firebase/components/Qualified.java @@ -0,0 +1,64 @@ +// Copyright 2022 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.components; + +import java.lang.annotation.Annotation; + +/** Represents a qualified class object. */ +public final class Qualified { + private @interface Unqualified {} + + private final Class qualifier; + private final Class type; + + public Qualified(Class qualifier, Class type) { + this.qualifier = qualifier; + this.type = type; + } + + public static Qualified unqualified(Class type) { + return new Qualified<>(Unqualified.class, type); + } + + public static Qualified qualified(Class qualifier, Class type) { + return new Qualified<>(qualifier, type); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Qualified qualified = (Qualified) o; + + if (!type.equals(qualified.type)) return false; + return qualifier.equals(qualified.qualifier); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + qualifier.hashCode(); + return result; + } + + @Override + public String toString() { + if (qualifier == Unqualified.class) { + return type.getName(); + } + return "@" + qualifier.getName() + " " + type.getName(); + } +} diff --git a/firebase-components/src/main/java/com/google/firebase/components/RestrictedComponentContainer.java b/firebase-components/src/main/java/com/google/firebase/components/RestrictedComponentContainer.java index 295dc3eb7cc..0c051e83a5b 100644 --- a/firebase-components/src/main/java/com/google/firebase/components/RestrictedComponentContainer.java +++ b/firebase-components/src/main/java/com/google/firebase/components/RestrictedComponentContainer.java @@ -26,21 +26,21 @@ * An implementation of {@link ComponentContainer} that is backed by another delegate {@link * ComponentContainer} and restricts access to only declared {@link Dependency dependencies}. */ -final class RestrictedComponentContainer extends AbstractComponentContainer { - private final Set> allowedDirectInterfaces; - private final Set> allowedProviderInterfaces; - private final Set> allowedDeferredInterfaces; - private final Set> allowedSetDirectInterfaces; - private final Set> allowedSetProviderInterfaces; +final class RestrictedComponentContainer implements ComponentContainer { + private final Set> allowedDirectInterfaces; + private final Set> allowedProviderInterfaces; + private final Set> allowedDeferredInterfaces; + private final Set> allowedSetDirectInterfaces; + private final Set> allowedSetProviderInterfaces; private final Set> allowedPublishedEvents; private final ComponentContainer delegateContainer; RestrictedComponentContainer(Component component, ComponentContainer container) { - Set> directInterfaces = new HashSet<>(); - Set> providerInterfaces = new HashSet<>(); - Set> deferredInterfaces = new HashSet<>(); - Set> setDirectInterfaces = new HashSet<>(); - Set> setProviderInterfaces = new HashSet<>(); + Set> directInterfaces = new HashSet<>(); + Set> providerInterfaces = new HashSet<>(); + Set> deferredInterfaces = new HashSet<>(); + Set> setDirectInterfaces = new HashSet<>(); + Set> setProviderInterfaces = new HashSet<>(); for (Dependency dependency : component.getDependencies()) { if (dependency.isDirectInjection()) { if (dependency.isSet()) { @@ -59,7 +59,7 @@ final class RestrictedComponentContainer extends AbstractComponentContainer { } } if (!component.getPublishedEvents().isEmpty()) { - directInterfaces.add(Publisher.class); + directInterfaces.add(Qualified.unqualified(Publisher.class)); } allowedDirectInterfaces = Collections.unmodifiableSet(directInterfaces); allowedProviderInterfaces = Collections.unmodifiableSet(providerInterfaces); @@ -77,7 +77,7 @@ final class RestrictedComponentContainer extends AbstractComponentContainer { */ @Override public T get(Class anInterface) { - if (!allowedDirectInterfaces.contains(anInterface)) { + if (!allowedDirectInterfaces.contains(Qualified.unqualified(anInterface))) { throw new DependencyException( String.format("Attempting to request an undeclared dependency %s.", anInterface)); } @@ -96,6 +96,15 @@ public T get(Class anInterface) { return publisher; } + @Override + public T get(Qualified anInterface) { + if (!allowedDirectInterfaces.contains(anInterface)) { + throw new DependencyException( + String.format("Attempting to request an undeclared dependency %s.", anInterface)); + } + return delegateContainer.get(anInterface); + } + /** * Returns an instance of the provider for the requested class if it is allowed. * @@ -103,6 +112,26 @@ public T get(Class anInterface) { */ @Override public Provider getProvider(Class anInterface) { + return getProvider(Qualified.unqualified(anInterface)); + } + + @Override + public Deferred getDeferred(Class anInterface) { + return getDeferred(Qualified.unqualified(anInterface)); + } + + /** + * Returns an instance of the provider for the set of requested classes if it is allowed. + * + * @throws DependencyException otherwise. + */ + @Override + public Provider> setOfProvider(Class anInterface) { + return setOfProvider(Qualified.unqualified(anInterface)); + } + + @Override + public Provider getProvider(Qualified anInterface) { if (!allowedProviderInterfaces.contains(anInterface)) { throw new DependencyException( String.format( @@ -112,7 +141,7 @@ public Provider getProvider(Class anInterface) { } @Override - public Deferred getDeferred(Class anInterface) { + public Deferred getDeferred(Qualified anInterface) { if (!allowedDeferredInterfaces.contains(anInterface)) { throw new DependencyException( String.format( @@ -121,13 +150,8 @@ public Deferred getDeferred(Class anInterface) { return delegateContainer.getDeferred(anInterface); } - /** - * Returns an instance of the provider for the set of requested classes if it is allowed. - * - * @throws DependencyException otherwise. - */ @Override - public Provider> setOfProvider(Class anInterface) { + public Provider> setOfProvider(Qualified anInterface) { if (!allowedSetProviderInterfaces.contains(anInterface)) { throw new DependencyException( String.format( @@ -142,7 +166,7 @@ public Provider> setOfProvider(Class anInterface) { * @throws DependencyException otherwise. */ @Override - public Set setOf(Class anInterface) { + public Set setOf(Qualified anInterface) { if (!allowedSetDirectInterfaces.contains(anInterface)) { throw new DependencyException( String.format("Attempting to request an undeclared dependency Set<%s>.", anInterface)); diff --git a/firebase-components/src/test/java/com/google/firebase/components/ComponentRuntimeTest.java b/firebase-components/src/test/java/com/google/firebase/components/ComponentRuntimeTest.java index ed5087cbdc9..8e445aee84a 100644 --- a/firebase-components/src/test/java/com/google/firebase/components/ComponentRuntimeTest.java +++ b/firebase-components/src/test/java/com/google/firebase/components/ComponentRuntimeTest.java @@ -15,6 +15,8 @@ package com.google.firebase.components; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.components.Qualified.qualified; +import static com.google.firebase.components.Qualified.unqualified; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; @@ -33,6 +35,10 @@ @RunWith(JUnit4.class) public final class ComponentRuntimeTest { + private @interface Qualifier1 {} + + private @interface Qualifier2 {} + private static final Executor EXECUTOR = Runnable::run; interface ComponentOne { @@ -210,6 +216,21 @@ public void container_withMultipleComponentsRegisteredForSameInterface_shouldThr } } + @Test + public void + container_withMultipleComponentsRegisteredForSameInterfaceButQualified_shouldNotThrow() { + ComponentRuntime runtime = + ComponentRuntime.builder(EXECUTOR) + .addComponent(Component.of(1, Integer.class)) + .addComponent(Component.of(2, qualified(Qualifier1.class, Integer.class))) + .addComponent(Component.of(3, qualified(Qualifier2.class, Integer.class))) + .build(); + + assertThat(runtime.get(Integer.class)).isEqualTo(1); + assertThat(runtime.get(qualified(Qualifier1.class, Integer.class))).isEqualTo(2); + assertThat(runtime.get(qualified(Qualifier2.class, Integer.class))).isEqualTo(3); + } + @Test public void container_withMissingDependencies_shouldThrow() { try { @@ -265,7 +286,7 @@ public void container_withCyclicProviderDependency_shouldProperlyInitialize() { public void get_withNullInterface_shouldThrow() { ComponentRuntime runtime = ComponentRuntime.builder(EXECUTOR).build(); try { - runtime.get(null); + runtime.get((Qualified) null); fail("Expected exception not thrown."); } catch (NullPointerException ex) { // success. @@ -350,6 +371,25 @@ public void setComponents_shouldNotPreventValueComponentsFromBeingRegistered() { assertThat(runtime.get(Double.class)).isEqualTo(4d); } + @Test + public void setComponents_withQualifiers_shouldContributeToAppropriateSets() { + ComponentRuntime runtime = + ComponentRuntime.builder(EXECUTOR) + .addComponent(Component.of(5, Integer.class)) + .addComponent(Component.intoSet(1, Integer.class)) + .addComponent(Component.intoSet(3, Integer.class)) + .addComponent(Component.intoSet(1, qualified(Qualifier1.class, Integer.class))) + .addComponent(Component.intoSet(2, qualified(Qualifier1.class, Integer.class))) + .addComponent(Component.intoSet(3, qualified(Qualifier2.class, Integer.class))) + .addComponent(Component.intoSet(4, qualified(Qualifier2.class, Integer.class))) + .build(); + + assertThat(runtime.get(Integer.class)).isEqualTo(5); + assertThat(runtime.setOf(Integer.class)).containsExactly(1, 3); + assertThat(runtime.setOf(qualified(Qualifier1.class, Integer.class))).containsExactly(1, 2); + assertThat(runtime.setOf(qualified(Qualifier2.class, Integer.class))).containsExactly(3, 4); + } + private static class DependsOnString { private final Provider dep; @@ -607,8 +647,8 @@ public void container_withComponentProcessor_shouldDelegateToItForEachComponentR runtime.getAllComponentsForTest().stream() .filter( c -> - (c.getProvidedInterfaces().contains(ComponentOne.class) - || c.getProvidedInterfaces().contains(ComponentTwo.class))) + (c.getProvidedInterfaces().contains(unqualified(ComponentOne.class)) + || c.getProvidedInterfaces().contains(unqualified(ComponentTwo.class)))) .allMatch(c -> c.getFactory() == replacedFactory)) .isTrue(); } diff --git a/firebase-components/src/test/java/com/google/firebase/components/ComponentTest.java b/firebase-components/src/test/java/com/google/firebase/components/ComponentTest.java index 92cc6cf3588..13549495ef8 100644 --- a/firebase-components/src/test/java/com/google/firebase/components/ComponentTest.java +++ b/firebase-components/src/test/java/com/google/firebase/components/ComponentTest.java @@ -15,6 +15,8 @@ package com.google.firebase.components; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.components.Qualified.qualified; +import static com.google.firebase.components.Qualified.unqualified; import static org.junit.Assert.fail; import java.math.BigDecimal; @@ -25,6 +27,8 @@ @RunWith(JUnit4.class) public class ComponentTest { + @interface TestQualifier {} + interface TestInterface {} private static class TestClass implements TestInterface {} @@ -36,7 +40,7 @@ public void of_withMultipleInterfaces_shouldSetCorrectDefaults() { TestClass testClass = new TestClass(); Component component = Component.of(testClass, TestClass.class, TestInterface.class); assertThat(component.getProvidedInterfaces()) - .containsExactly(TestClass.class, TestInterface.class); + .containsExactly(unqualified(TestClass.class), unqualified(TestInterface.class)); assertThat(component.isLazy()).isTrue(); assertThat(component.isValue()).isTrue(); assertThat(component.isAlwaysEager()).isFalse(); @@ -49,7 +53,22 @@ public void of_withMultipleInterfaces_shouldSetCorrectDefaults() { public void builder_shouldSetCorrectDefaults() { Component component = Component.builder(TestClass.class).factory(nullFactory).build(); - assertThat(component.getProvidedInterfaces()).containsExactly(TestClass.class); + assertThat(component.getProvidedInterfaces()).containsExactly(unqualified(TestClass.class)); + assertThat(component.isLazy()).isTrue(); + assertThat(component.isValue()).isTrue(); + assertThat(component.isAlwaysEager()).isFalse(); + assertThat(component.isEagerInDefaultApp()).isFalse(); + assertThat(component.getDependencies()).isEmpty(); + } + + @Test + public void qualifiedBuilder_shouldSetCorrectDefaults() { + Component component = + Component.builder(qualified(TestQualifier.class, TestClass.class)) + .factory(nullFactory) + .build(); + assertThat(component.getProvidedInterfaces()) + .containsExactly(qualified(TestQualifier.class, TestClass.class)); assertThat(component.isLazy()).isTrue(); assertThat(component.isValue()).isTrue(); assertThat(component.isAlwaysEager()).isFalse(); @@ -61,7 +80,7 @@ public void builder_shouldSetCorrectDefaults() { public void intoSetBuilder_shouldSetCorrectDefaults() { Component component = Component.intoSetBuilder(TestClass.class).factory(nullFactory).build(); - assertThat(component.getProvidedInterfaces()).containsExactly(TestClass.class); + assertThat(component.getProvidedInterfaces()).containsExactly(unqualified(TestClass.class)); assertThat(component.isLazy()).isTrue(); assertThat(component.isValue()).isFalse(); assertThat(component.isAlwaysEager()).isFalse(); @@ -73,7 +92,7 @@ public void intoSetBuilder_shouldSetCorrectDefaults() { public void intoSet_shouldSetCorrectDefaults() { TestClass testClass = new TestClass(); Component component = Component.intoSet(testClass, TestClass.class); - assertThat(component.getProvidedInterfaces()).containsExactly(TestClass.class); + assertThat(component.getProvidedInterfaces()).containsExactly(unqualified(TestClass.class)); assertThat(component.isLazy()).isTrue(); assertThat(component.isValue()).isFalse(); assertThat(component.isAlwaysEager()).isFalse(); @@ -174,7 +193,21 @@ public void builder_withMultipleInterfaces_shouldProperlySetInterfaces() { Component component = Component.builder(TestClass.class, TestInterface.class).factory(nullFactory).build(); assertThat(component.getProvidedInterfaces()) - .containsExactly(TestClass.class, TestInterface.class); + .containsExactly(unqualified(TestClass.class), unqualified(TestInterface.class)); + } + + @Test + public void builder_withMultipleQualifiedInterfaces_shouldProperlySetInterfaces() { + Component component = + Component.builder( + qualified(TestQualifier.class, TestClass.class), + qualified(TestQualifier.class, TestInterface.class)) + .factory(nullFactory) + .build(); + assertThat(component.getProvidedInterfaces()) + .containsExactly( + qualified(TestQualifier.class, TestClass.class), + qualified(TestQualifier.class, TestInterface.class)); } @Test diff --git a/firebase-components/src/test/java/com/google/firebase/components/CycleDetectorTest.java b/firebase-components/src/test/java/com/google/firebase/components/CycleDetectorTest.java index 091b7be8b08..8fc904741fe 100644 --- a/firebase-components/src/test/java/com/google/firebase/components/CycleDetectorTest.java +++ b/firebase-components/src/test/java/com/google/firebase/components/CycleDetectorTest.java @@ -15,11 +15,13 @@ package com.google.firebase.components; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.components.Qualified.qualified; import static junit.framework.Assert.fail; import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -246,7 +248,7 @@ public void detect_withProviderDependencyCycle_shouldNotThrow() { } @Test - public void detect_withMultipleComponentsImplementingSameIface_shouldThrow() { + public void detect_withMultipleComponentsImplementingSameInterface_shouldThrow() { List> components = Arrays.asList( Component.builder(TestInterface1.class).factory(nullFactory()).build(), @@ -260,6 +262,45 @@ public void detect_withMultipleComponentsImplementingSameIface_shouldThrow() { } } + @Qualifier + @interface Qualifier1 {} + + @Qualifier + @interface Qualifier2 {} + + @Test + public void detect_withMultipleComponentsImplementingSameQualifiedInterface_shouldNotThrow() { + List> components = + Arrays.asList( + Component.builder(TestInterface1.class).factory(nullFactory()).build(), + Component.builder(qualified(Qualifier1.class, TestInterface1.class)) + .factory(nullFactory()) + .build(), + Component.builder(qualified(Qualifier2.class, TestInterface1.class)) + .factory(nullFactory()) + .build()); + + CycleDetector.detect(components); + } + + @Test + public void + detect_withMultipleComponentsImplementingSameQualifiedInterfaceAndNoDepCycle_shouldNotThrow() { + List> components = + Arrays.asList( + Component.builder(TestInterface1.class).factory(nullFactory()).build(), + Component.builder(qualified(Qualifier1.class, TestInterface1.class)) + .add(Dependency.required(TestInterface1.class)) + .factory(nullFactory()) + .build(), + Component.builder(qualified(Qualifier2.class, TestInterface1.class)) + .add(Dependency.required(qualified(Qualifier1.class, TestInterface1.class))) + .factory(nullFactory()) + .build()); + + CycleDetector.detect(components); + } + private static void detect(List> components) { Collections.shuffle(components); try { diff --git a/firebase-components/src/test/java/com/google/firebase/components/DependencyTest.java b/firebase-components/src/test/java/com/google/firebase/components/DependencyTest.java index 0c3fdaca99c..9c13a0a9fd8 100644 --- a/firebase-components/src/test/java/com/google/firebase/components/DependencyTest.java +++ b/firebase-components/src/test/java/com/google/firebase/components/DependencyTest.java @@ -15,11 +15,17 @@ package com.google.firebase.components; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.components.Qualified.qualified; +import static com.google.firebase.components.Qualified.unqualified; +import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +@Qualifier +@interface TestQualifier {} + @RunWith(JUnit4.class) public class DependencyTest { @Test @@ -29,7 +35,7 @@ public void optional_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isFalse(); assertThat(dependency.isSet()).isFalse(); assertThat(dependency.isDirectInjection()).isTrue(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); } @Test @@ -39,7 +45,17 @@ public void required_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isTrue(); assertThat(dependency.isSet()).isFalse(); assertThat(dependency.isDirectInjection()).isTrue(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); + } + + @Test + public void requiredQualified_shouldHaveExpectedInvariants() { + Dependency dependency = Dependency.required(qualified(TestQualifier.class, String.class)); + + assertThat(dependency.isRequired()).isTrue(); + assertThat(dependency.isSet()).isFalse(); + assertThat(dependency.isDirectInjection()).isTrue(); + assertThat(dependency.getInterface()).isEqualTo(qualified(TestQualifier.class, String.class)); } @Test @@ -49,7 +65,17 @@ public void setOf_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isFalse(); assertThat(dependency.isSet()).isTrue(); assertThat(dependency.isDirectInjection()).isTrue(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); + } + + @Test + public void setOfQualified_shouldHaveExpectedInvariants() { + Dependency dependency = Dependency.setOf(qualified(TestQualifier.class, String.class)); + + assertThat(dependency.isRequired()).isFalse(); + assertThat(dependency.isSet()).isTrue(); + assertThat(dependency.isDirectInjection()).isTrue(); + assertThat(dependency.getInterface()).isEqualTo(qualified(TestQualifier.class, String.class)); } @Test @@ -59,7 +85,18 @@ public void optionalProvider_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isFalse(); assertThat(dependency.isSet()).isFalse(); assertThat(dependency.isDirectInjection()).isFalse(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); + } + + @Test + public void optionalProviderQualified_shouldHaveExpectedInvariants() { + Dependency dependency = + Dependency.optionalProvider(qualified(TestQualifier.class, String.class)); + + assertThat(dependency.isRequired()).isFalse(); + assertThat(dependency.isSet()).isFalse(); + assertThat(dependency.isDirectInjection()).isFalse(); + assertThat(dependency.getInterface()).isEqualTo(qualified(TestQualifier.class, String.class)); } @Test @@ -69,7 +106,18 @@ public void requiredProvider_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isTrue(); assertThat(dependency.isSet()).isFalse(); assertThat(dependency.isDirectInjection()).isFalse(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); + } + + @Test + public void requiredProviderQualified_shouldHaveExpectedInvariants() { + Dependency dependency = + Dependency.requiredProvider(qualified(TestQualifier.class, String.class)); + + assertThat(dependency.isRequired()).isTrue(); + assertThat(dependency.isSet()).isFalse(); + assertThat(dependency.isDirectInjection()).isFalse(); + assertThat(dependency.getInterface()).isEqualTo(qualified(TestQualifier.class, String.class)); } @Test @@ -79,6 +127,16 @@ public void setOfProvider_shouldHaveExpectedInvariants() { assertThat(dependency.isRequired()).isFalse(); assertThat(dependency.isSet()).isTrue(); assertThat(dependency.isDirectInjection()).isFalse(); - assertThat(dependency.getInterface()).isEqualTo(String.class); + assertThat(dependency.getInterface()).isEqualTo(unqualified(String.class)); + } + + @Test + public void setOfProviderQualified_shouldHaveExpectedInvariants() { + Dependency dependency = Dependency.setOfProvider(qualified(TestQualifier.class, String.class)); + + assertThat(dependency.isRequired()).isFalse(); + assertThat(dependency.isSet()).isTrue(); + assertThat(dependency.isDirectInjection()).isFalse(); + assertThat(dependency.getInterface()).isEqualTo(qualified(TestQualifier.class, String.class)); } } diff --git a/firebase-components/src/test/java/com/google/firebase/components/RestrictedComponentContainerTest.java b/firebase-components/src/test/java/com/google/firebase/components/RestrictedComponentContainerTest.java index bda8a135cd5..8367780f302 100644 --- a/firebase-components/src/test/java/com/google/firebase/components/RestrictedComponentContainerTest.java +++ b/firebase-components/src/test/java/com/google/firebase/components/RestrictedComponentContainerTest.java @@ -15,9 +15,11 @@ package com.google.firebase.components; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.components.Qualified.unqualified; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -96,10 +98,10 @@ public void get_withPublisher_shouldThrow() { @Test public void getProvider_withAllowedClass_shouldReturnAnInstanceOfThatClass() { Double value = 3.0d; - when(delegate.getProvider(Double.class)).thenReturn(new Lazy<>(value)); + when(delegate.getProvider(unqualified(Double.class))).thenReturn(new Lazy<>(value)); assertThat(container.getProvider(Double.class).get()).isSameInstanceAs(value); - verify(delegate).getProvider(Double.class); + verify(delegate).getProvider(unqualified(Double.class)); } @Test @@ -125,12 +127,16 @@ public void getProvider_withDirectClass_shouldThrow() { @Test public void getDeferred_withAllowedClass_shouldReturnAnInstanceOfThatClass() { Integer value = 3; - when(delegate.getDeferred(Integer.class)).thenReturn(OptionalProvider.of(() -> value)); + when(delegate.getDeferred(unqualified(Integer.class))) + .thenReturn(OptionalProvider.of(() -> value)); AtomicReference returned = new AtomicReference<>(); container.getDeferred(Integer.class).whenAvailable(d -> returned.set(d.get())); assertThat(returned.get()).isSameInstanceAs(value); - verify(delegate).getDeferred(Integer.class); + + container.getDeferred(unqualified(Integer.class)).whenAvailable(d -> returned.set(d.get())); + assertThat(returned.get()).isSameInstanceAs(value); + verify(delegate, times(2)).getDeferred(unqualified(Integer.class)); } @Test @@ -166,10 +172,11 @@ public void getDeferred_withProviderClass_shouldThrow() { @Test public void setOf_withAllowedClass_shouldReturnExpectedSet() { Set set = Collections.emptySet(); - when(delegate.setOf(Long.class)).thenReturn(set); + when(delegate.setOf(unqualified(Long.class))).thenReturn(set); assertThat(container.setOf(Long.class)).isSameInstanceAs(set); - verify(delegate).setOf(Long.class); + assertThat(container.setOf(unqualified(Long.class))).isSameInstanceAs(set); + verify(delegate, times(2)).setOf(unqualified(Long.class)); } @Test @@ -185,10 +192,11 @@ public void setOf_withNotAllowedClass_shouldThrow() { @Test public void setOfProvider_withAllowedClass_shouldReturnExpectedSet() { Set set = Collections.emptySet(); - when(delegate.setOfProvider(Boolean.class)).thenReturn(new Lazy<>(set)); + when(delegate.setOfProvider(unqualified(Boolean.class))).thenReturn(new Lazy<>(set)); assertThat(container.setOfProvider(Boolean.class).get()).isSameInstanceAs(set); - verify(delegate).setOfProvider(Boolean.class); + assertThat(container.setOfProvider(unqualified(Boolean.class)).get()).isSameInstanceAs(set); + verify(delegate, times(2)).setOfProvider(unqualified(Boolean.class)); } @Test diff --git a/firebase-config/firebase-config.gradle b/firebase-config/firebase-config.gradle index 5f48b40ec56..0613e50cdc8 100644 --- a/firebase-config/firebase-config.gradle +++ b/firebase-config/firebase-config.gradle @@ -49,6 +49,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-abt') implementation project(':firebase-components') diff --git a/firebase-config/gradle.properties b/firebase-config/gradle.properties index aacbd727d0e..5a8818a73fa 100644 --- a/firebase-config/gradle.properties +++ b/firebase-config/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # -version=21.1.3 -latestReleasedVersion=21.1.2 +version=21.2.1 +latestReleasedVersion=21.2.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt b/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt index 5df64733d43..21a48c4d0a7 100644 --- a/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt +++ b/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt @@ -26,20 +26,23 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue /** Returns the [FirebaseRemoteConfig] instance of the default [FirebaseApp]. */ val Firebase.remoteConfig: FirebaseRemoteConfig - get() = FirebaseRemoteConfig.getInstance() + get() = FirebaseRemoteConfig.getInstance() /** Returns the [FirebaseRemoteConfig] instance of a given [FirebaseApp]. */ -fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance(app) +fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = + FirebaseRemoteConfig.getInstance(app) /** See [FirebaseRemoteConfig#getValue] */ operator fun FirebaseRemoteConfig.get(key: String): FirebaseRemoteConfigValue { - return this.getValue(key) + return this.getValue(key) } -fun remoteConfigSettings(init: FirebaseRemoteConfigSettings.Builder.() -> Unit): FirebaseRemoteConfigSettings { - val builder = FirebaseRemoteConfigSettings.Builder() - builder.init() - return builder.build() +fun remoteConfigSettings( + init: FirebaseRemoteConfigSettings.Builder.() -> Unit +): FirebaseRemoteConfigSettings { + val builder = FirebaseRemoteConfigSettings.Builder() + builder.init() + return builder.build() } internal const val LIBRARY_NAME: String = "fire-cfg-ktx" @@ -47,6 +50,6 @@ internal const val LIBRARY_NAME: String = "fire-cfg-ktx" /** @suppress */ @Keep class FirebaseRemoteConfigKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt index 0174ed347f9..ed6b385ab4f 100644 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt +++ b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt @@ -28,31 +28,31 @@ import java.util.concurrent.Executor // This method is a workaround for testing. It enable us to create a FirebaseRemoteConfig object // with mocks using the package-private constructor. fun createRemoteConfig( - context: Context?, - firebaseApp: FirebaseApp, - firebaseInstallations: FirebaseInstallationsApi, - firebaseAbt: FirebaseABTesting?, - executor: Executor, - fetchedConfigsCache: ConfigCacheClient, - activatedConfigsCache: ConfigCacheClient, - defaultConfigsCache: ConfigCacheClient, - fetchHandler: ConfigFetchHandler, - getHandler: ConfigGetParameterHandler, - frcMetadata: ConfigMetadataClient, - realtimeClient: ConfigRealtimeHandler + context: Context?, + firebaseApp: FirebaseApp, + firebaseInstallations: FirebaseInstallationsApi, + firebaseAbt: FirebaseABTesting?, + executor: Executor, + fetchedConfigsCache: ConfigCacheClient, + activatedConfigsCache: ConfigCacheClient, + defaultConfigsCache: ConfigCacheClient, + fetchHandler: ConfigFetchHandler, + getHandler: ConfigGetParameterHandler, + frcMetadata: ConfigMetadataClient, + realtimeHandler: ConfigRealtimeHandler ): FirebaseRemoteConfig { - return FirebaseRemoteConfig( - context, - firebaseApp, - firebaseInstallations, - firebaseAbt, - executor, - fetchedConfigsCache, - activatedConfigsCache, - defaultConfigsCache, - fetchHandler, - getHandler, - frcMetadata, - realtimeClient - ) + return FirebaseRemoteConfig( + context, + firebaseApp, + firebaseInstallations, + firebaseAbt, + executor, + fetchedConfigsCache, + activatedConfigsCache, + defaultConfigsCache, + fetchHandler, + getHandler, + frcMetadata, + realtimeHandler + ) } diff --git a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt index 443395eb942..b4a5c8eb9bd 100644 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt +++ b/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt @@ -36,8 +36,8 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner const val APP_ID = "1:14368190084:android:09cb977358c6f241" @@ -46,112 +46,113 @@ const val API_KEY = "AIzaSyabcdefghijklmnopqrstuvwxyz1234567" const val EXISTING_APP = "existing" open class DefaultFirebaseRemoteConfigValue : FirebaseRemoteConfigValue { - override fun asLong(): Long = TODO("Unimplementend") - override fun asDouble(): Double = TODO("Unimplementend") - override fun asString(): String = TODO("Unimplementend") - override fun asByteArray(): ByteArray = TODO("Unimplementend") - override fun asBoolean(): Boolean = TODO("Unimplementend") - override fun getSource(): Int = TODO("Unimplementend") + override fun asLong(): Long = TODO("Unimplementend") + override fun asDouble(): Double = TODO("Unimplementend") + override fun asString(): String = TODO("Unimplementend") + override fun asByteArray(): ByteArray = TODO("Unimplementend") + override fun asBoolean(): Boolean = TODO("Unimplementend") + override fun getSource(): Int = TODO("Unimplementend") } class StringRemoteConfigValue(val value: String) : DefaultFirebaseRemoteConfigValue() { - override fun asString() = value + override fun asString() = value } abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class ConfigTests : BaseTestCase() { - @Test - fun `Firebase#remoteConfig should delegate to FirebaseRemoteConfig#getInstance()`() { - assertThat(Firebase.remoteConfig).isSameInstanceAs(FirebaseRemoteConfig.getInstance()) - } - - @Test - fun `Firebase#remoteConfig should delegate to FirebaseRemoteConfig#getInstance(FirebaseApp, region)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.remoteConfig(app)).isSameInstanceAs(FirebaseRemoteConfig.getInstance(app)) - } - - @Test - fun `Overloaded get() operator returns default value when key doesn't exist`() { - val remoteConfig = Firebase.remoteConfig - assertThat(remoteConfig["non_existing_key"].asString()) - .isEqualTo(FirebaseRemoteConfig.DEFAULT_VALUE_FOR_STRING) - assertThat(remoteConfig["another_non_exisiting_key"].asDouble()) - .isEqualTo(FirebaseRemoteConfig.DEFAULT_VALUE_FOR_DOUBLE) - } - - @Test - fun `FirebaseRemoteConfigSettings builder works`() { - val minFetchInterval = 3600L - val fetchTimeout = 60L - val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = minFetchInterval - fetchTimeoutInSeconds = fetchTimeout - } - assertThat(configSettings.minimumFetchIntervalInSeconds).isEqualTo(minFetchInterval) - assertThat(configSettings.fetchTimeoutInSeconds).isEqualTo(fetchTimeout) - } - - @Test - fun `Overloaded get() operator returns value when key exists`() { - val mockGetHandler = mock(ConfigGetParameterHandler::class.java) - val directExecutor = MoreExecutors.directExecutor() - - val remoteConfig = createRemoteConfig( - context = null, - firebaseApp = Firebase.app(EXISTING_APP), - firebaseInstallations = mock(FirebaseInstallationsApi::class.java), - firebaseAbt = null, - executor = directExecutor, - fetchedConfigsCache = mock(ConfigCacheClient::class.java), - activatedConfigsCache = mock(ConfigCacheClient::class.java), - defaultConfigsCache = mock(ConfigCacheClient::class.java), - fetchHandler = mock(ConfigFetchHandler::class.java), - getHandler = mockGetHandler, - frcMetadata = mock(ConfigMetadataClient::class.java), - realtimeClient = mock(ConfigRealtimeHandler::class.java) - ) - - `when`(mockGetHandler.getValue("KEY")).thenReturn(StringRemoteConfigValue("non default value")) - assertThat(remoteConfig["KEY"].asString()).isEqualTo("non default value") + @Test + fun `Firebase#remoteConfig should delegate to FirebaseRemoteConfig#getInstance()`() { + assertThat(Firebase.remoteConfig).isSameInstanceAs(FirebaseRemoteConfig.getInstance()) + } + + @Test + fun `Firebase#remoteConfig should delegate to FirebaseRemoteConfig#getInstance(FirebaseApp, region)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.remoteConfig(app)).isSameInstanceAs(FirebaseRemoteConfig.getInstance(app)) + } + + @Test + fun `Overloaded get() operator returns default value when key doesn't exist`() { + val remoteConfig = Firebase.remoteConfig + assertThat(remoteConfig["non_existing_key"].asString()) + .isEqualTo(FirebaseRemoteConfig.DEFAULT_VALUE_FOR_STRING) + assertThat(remoteConfig["another_non_exisiting_key"].asDouble()) + .isEqualTo(FirebaseRemoteConfig.DEFAULT_VALUE_FOR_DOUBLE) + } + + @Test + fun `FirebaseRemoteConfigSettings builder works`() { + val minFetchInterval = 3600L + val fetchTimeout = 60L + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = minFetchInterval + fetchTimeoutInSeconds = fetchTimeout } + assertThat(configSettings.minimumFetchIntervalInSeconds).isEqualTo(minFetchInterval) + assertThat(configSettings.fetchTimeoutInSeconds).isEqualTo(fetchTimeout) + } + + @Test + fun `Overloaded get() operator returns value when key exists`() { + val mockGetHandler = mock(ConfigGetParameterHandler::class.java) + val directExecutor = MoreExecutors.directExecutor() + + val remoteConfig = + createRemoteConfig( + context = null, + firebaseApp = Firebase.app(EXISTING_APP), + firebaseInstallations = mock(FirebaseInstallationsApi::class.java), + firebaseAbt = null, + executor = directExecutor, + fetchedConfigsCache = mock(ConfigCacheClient::class.java), + activatedConfigsCache = mock(ConfigCacheClient::class.java), + defaultConfigsCache = mock(ConfigCacheClient::class.java), + fetchHandler = mock(ConfigFetchHandler::class.java), + getHandler = mockGetHandler, + frcMetadata = mock(ConfigMetadataClient::class.java), + realtimeHandler = mock(ConfigRealtimeHandler::class.java) + ) + + `when`(mockGetHandler.getValue("KEY")).thenReturn(StringRemoteConfigValue("non default value")) + assertThat(remoteConfig["KEY"].asString()).isEqualTo("non default value") + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index b446adfb8d6..a1cb96e4f54 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -25,6 +25,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.abt.AbtException; import com.google.firebase.abt.FirebaseABTesting; +import com.google.firebase.concurrent.FirebaseExecutors; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.remoteconfig.internal.ConfigCacheClient; @@ -288,7 +289,8 @@ public Task fetch() { Task fetchTask = fetchHandler.fetch(); // Convert Task type to Void. - return fetchTask.onSuccessTask((unusedFetchResponse) -> Tasks.forResult(null)); + return fetchTask.onSuccessTask( + FirebaseExecutors.directExecutor(), (unusedFetchResponse) -> Tasks.forResult(null)); } /** @@ -315,7 +317,8 @@ public Task fetch(long minimumFetchIntervalInSeconds) { Task fetchTask = fetchHandler.fetch(minimumFetchIntervalInSeconds); // Convert Task type to Void. - return fetchTask.onSuccessTask((unusedFetchResponse) -> Tasks.forResult(null)); + return fetchTask.onSuccessTask( + FirebaseExecutors.directExecutor(), (unusedFetchResponse) -> Tasks.forResult(null)); } /** @@ -612,7 +615,8 @@ private Task setDefaultsWithStringsMapAsync(Map defaultsSt Task putTask = defaultConfigsCache.put(defaultConfigs); // Convert Task type to Void. - return putTask.onSuccessTask((unusedContainer) -> Tasks.forResult(null)); + return putTask.onSuccessTask( + FirebaseExecutors.directExecutor(), (unusedContainer) -> Tasks.forResult(null)); } /** diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java index c90e9f16eeb..e0e675029d9 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigComponent.java @@ -28,6 +28,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.abt.FirebaseABTesting; import com.google.firebase.analytics.connector.AnalyticsConnector; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.inject.Provider; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.remoteconfig.internal.ConfigCacheClient; @@ -42,8 +43,6 @@ import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -85,9 +84,7 @@ public class RemoteConfigComponent { new HashMap<>(); private final Context context; - // TODO: Consolidate executors. - private final ExecutorService executorService; - private final ScheduledExecutorService scheduledExecutorService; + private final ScheduledExecutorService executor; private final FirebaseApp firebaseApp; private final FirebaseInstallationsApi firebaseInstallations; private final FirebaseABTesting firebaseAbt; @@ -101,14 +98,14 @@ public class RemoteConfigComponent { /** Firebase Remote Config Component constructor. */ RemoteConfigComponent( Context context, + @Blocking ScheduledExecutorService executor, FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallations, FirebaseABTesting firebaseAbt, Provider analyticsConnector) { this( context, - Executors.newCachedThreadPool(), - Executors.newSingleThreadScheduledExecutor(), + executor, firebaseApp, firebaseInstallations, firebaseAbt, @@ -120,16 +117,14 @@ public class RemoteConfigComponent { @VisibleForTesting protected RemoteConfigComponent( Context context, - ExecutorService executorService, - ScheduledExecutorService scheduledExecutorService, + ScheduledExecutorService executor, FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallations, FirebaseABTesting firebaseAbt, Provider analyticsConnector, boolean loadGetDefault) { this.context = context; - this.executorService = executorService; - this.scheduledExecutorService = scheduledExecutorService; + this.executor = executor; this.firebaseApp = firebaseApp; this.firebaseInstallations = firebaseInstallations; this.firebaseAbt = firebaseAbt; @@ -143,7 +138,7 @@ protected RemoteConfigComponent( // while another test has already cleared the component but hasn't gotten a new one yet. if (loadGetDefault) { // Loads the default namespace's configs from disk on App startup. - Tasks.call(executorService, this::getDefault); + Tasks.call(executor, this::getDefault); } } @@ -180,7 +175,7 @@ public synchronized FirebaseRemoteConfig get(String namespace) { namespace, firebaseInstallations, firebaseAbt, - executorService, + executor, fetchedCacheClient, activatedCacheClient, defaultsCacheClient, @@ -241,7 +236,7 @@ private ConfigCacheClient getCacheClient(String namespace, String configStoreTyp "%s_%s_%s_%s.json", FIREBASE_REMOTE_CONFIG_FILE_NAME_PREFIX, appId, namespace, configStoreType); return ConfigCacheClient.getInstance( - Executors.newCachedThreadPool(), ConfigStorageClient.getInstance(context, fileName)); + executor, ConfigStorageClient.getInstance(context, fileName)); } @VisibleForTesting @@ -263,7 +258,7 @@ synchronized ConfigFetchHandler getFetchHandler( return new ConfigFetchHandler( firebaseInstallations, isPrimaryApp(firebaseApp) ? analyticsConnector : () -> null, - executorService, + executor, DEFAULT_CLOCK, DEFAULT_RANDOM, fetchedCacheClient, @@ -286,13 +281,12 @@ synchronized ConfigRealtimeHandler getRealtime( activatedCacheClient, context, namespace, - scheduledExecutorService); + executor); } private ConfigGetParameterHandler getGetHandler( ConfigCacheClient activatedCacheClient, ConfigCacheClient defaultsCacheClient) { - return new ConfigGetParameterHandler( - executorService, activatedCacheClient, defaultsCacheClient); + return new ConfigGetParameterHandler(executor, activatedCacheClient, defaultsCacheClient); } @VisibleForTesting diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigRegistrar.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigRegistrar.java index d856c9d573d..9fe5cb8bf14 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigRegistrar.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigRegistrar.java @@ -20,13 +20,16 @@ import com.google.firebase.abt.FirebaseABTesting.OriginService; import com.google.firebase.abt.component.AbtComponent; import com.google.firebase.analytics.connector.AnalyticsConnector; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; /** * Registrar for setting up Firebase Remote Config's dependency injections in Firebase Android @@ -41,10 +44,13 @@ public class RemoteConfigRegistrar implements ComponentRegistrar { @Override public List> getComponents() { + Qualified blockingExecutor = + Qualified.qualified(Blocking.class, ScheduledExecutorService.class); return Arrays.asList( Component.builder(RemoteConfigComponent.class) .name(LIBRARY_NAME) .add(Dependency.required(Context.class)) + .add(Dependency.required(blockingExecutor)) .add(Dependency.required(FirebaseApp.class)) .add(Dependency.required(FirebaseInstallationsApi.class)) .add(Dependency.required(AbtComponent.class)) @@ -53,6 +59,7 @@ public List> getComponents() { container -> new RemoteConfigComponent( container.get(Context.class), + container.get(blockingExecutor), container.get(FirebaseApp.class), container.get(FirebaseInstallationsApi.class), container.get(AbtComponent.class).get(OriginService.REMOTE_CONFIG), diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigCacheClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigCacheClient.java index 3cb18e36a4c..ea504a85f46 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigCacheClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigCacheClient.java @@ -32,7 +32,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -56,7 +55,7 @@ public class ConfigCacheClient { @GuardedBy("ConfigCacheClient.class") private static final Map clientInstances = new HashMap<>(); - private final ExecutorService executorService; + private final Executor executor; private final ConfigStorageClient storageClient; /** @@ -71,8 +70,8 @@ public class ConfigCacheClient { * Creates a new cache client that executes async calls through {@code executorService} and is * backed by {@code storageClient}. */ - private ConfigCacheClient(ExecutorService executorService, ConfigStorageClient storageClient) { - this.executorService = executorService; + private ConfigCacheClient(Executor executor, ConfigStorageClient storageClient) { + this.executor = executor; this.storageClient = storageClient; cachedContainerTask = null; @@ -126,9 +125,9 @@ public Task put(ConfigContainer configContainer) { */ public Task put( ConfigContainer configContainer, boolean shouldUpdateInMemoryContainer) { - return Tasks.call(executorService, () -> storageClient.write(configContainer)) + return Tasks.call(executor, () -> storageClient.write(configContainer)) .onSuccessTask( - executorService, + executor, (unusedVoid) -> { if (shouldUpdateInMemoryContainer) { updateInMemoryConfigContainer(configContainer); @@ -165,7 +164,7 @@ public synchronized Task get() { */ if (cachedContainerTask == null || (cachedContainerTask.isComplete() && !cachedContainerTask.isSuccessful())) { - cachedContainerTask = Tasks.call(executorService, storageClient::read); + cachedContainerTask = Tasks.call(executor, storageClient::read); } return cachedContainerTask; } @@ -200,10 +199,10 @@ synchronized Task getCachedContainerTask() { * underlying file name. */ public static synchronized ConfigCacheClient getInstance( - ExecutorService executorService, ConfigStorageClient storageClient) { + Executor executor, ConfigStorageClient storageClient) { String fileName = storageClient.getFileName(); if (!clientInstances.containsKey(fileName)) { - clientInstances.put(fileName, new ConfigCacheClient(executorService, storageClient)); + clientInstances.put(fileName, new ConfigCacheClient(executor, storageClient)); } return clientInstances.get(fileName); } diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java index 6d74b3acb59..f40beca396c 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/RemoteConfigComponentTest.java @@ -175,7 +175,6 @@ public void getFetchHandler_nonMainFirebaseApp_doesNotUseAnalytics() { private RemoteConfigComponent getNewFrcComponent() { return new RemoteConfigComponent( context, - directExecutor, scheduledExecutorService, mockFirebaseApp, mockFirebaseInstallations, @@ -187,7 +186,6 @@ private RemoteConfigComponent getNewFrcComponent() { private RemoteConfigComponent getNewFrcComponentWithoutLoadingDefault() { return new RemoteConfigComponent( context, - directExecutor, scheduledExecutorService, mockFirebaseApp, mockFirebaseInstallations, diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index 63ac39c0889..169c787127b 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,7 +1,24 @@ # Unreleased -# 18.2.14 -* [changed] Updated `firebase-crashlytics` dependency to v18.2.14. +# 18.3.2 +* [fixed] Fixed an [issue](https://github.com/firebase/firebase-android-sdk/issues/4313){: .external} + preventing native crashes from being reported for Android API 29+. + +# 18.3.1 +Warning: We're aware of an +[issue](https://github.com/firebase/firebase-android-sdk/issues/4313){: .external} +in this version of the [crashlytics] SDK for NDK.
**We strongly +recommend using the latest version of the SDK (v18.3.2+ or [bom] v31.0.3+).** + +* [changed] Updated `firebase-crashlytics` dependency to v18.3.1. + +# 18.3.0 +Warning: We're aware of an +[issue](https://github.com/firebase/firebase-android-sdk/issues/4223){: .external} +in the [crashlytics] Android SDK v18.3.0.
**We strongly recommend +using the latest version of the SDK (v18.3.1+ or [bom] v31.0.1+).** + +* [changed] Updated `firebase-crashlytics` dependency to v18.3.0. # 18.2.13 * [changed] Updated dependency of `play-services-basement` to its latest diff --git a/firebase-crashlytics-ndk/README.md b/firebase-crashlytics-ndk/README.md index b30f8acf25a..3c7538d8650 100644 --- a/firebase-crashlytics-ndk/README.md +++ b/firebase-crashlytics-ndk/README.md @@ -11,15 +11,10 @@ Initialize them by running the following commands: ## Building -* `firebase-crashlytics-ndk` must be built with NDK 21. Use Android Studio's - SDK Manager to ensure you have the appropriate NDK version installed, and - edit `../local.properties` to specify which NDK version to use when building - this project. For example: - `ndk.dir=$USER_HOME/Library/Android/sdk/ndk/21.4.7075529` * All Gradle commands should be run from the root of this repository: `./gradlew :firebase-crashlytics-ndk:assemble` ## Running Tests Integration tests, requiring a running and connected device (emulator or real): -`./gradlew :firebase-crashlytics-ndk:connectedAndroidTest` \ No newline at end of file +`./gradlew :firebase-crashlytics-ndk:connectedAndroidTest` diff --git a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle index 413e7dbcd2b..035dc852962 100644 --- a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle +++ b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle @@ -27,6 +27,7 @@ android { timeOutInMs 60 * 1000 } + ndkVersion "25.1.8937393" compileSdkVersion project.targetSdkVersion defaultConfig { minSdkVersion 16 @@ -65,45 +66,31 @@ android { } } + // There is not any normal way to package native executables in an Android APK. + // It is normal to package native code as a loadable module but Android's APK + // installer will ignore files not named like a shared object, so give the + // handler executable an acceptable name libraryVariants.all { variant -> - variant.outputs.each { output -> - def func = fixTrampolineFilenames(variant.baseName) - - tasks.findAll { - it.name.startsWith("bundleReleaseAar") - }.each { - it.dependsOn func + def fixTasks = ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"].collect { arch -> + tasks.register("fixTrampolineFilenames${variant.baseName}${arch}", com.google.firebase.gradle.NdkBinaryFixTask) { + it.inputFile = + file("${buildDir}/intermediates/ndkBuild/${variant.baseName}/obj/local/${arch}/crashlytics-trampoline") } + } - tasks.findAll { - it.name.startsWith("externalNativeBuild") && !it.name.contains("Clean") - }.each { - func.dependsOn it + tasks.withType(com.android.build.gradle.tasks.BundleAar) { + if (it.variantName != variant.baseName) return + fixTasks.each { fix -> + it.dependsOn fix + it.from(fix.map { it.outputFile }) { + into fix.map { it.into } + } } } - } -} - - -import java.nio.file.Files -import java.nio.file.StandardCopyOption - -// There is not any normal way to package native executables in an Android APK. -// It is normal to package native code as a loadable module but Android's APK -// installer will ignore files not named like a shared object, so give the -// handler executable an acceptable name -def fixTrampolineFilenames(variantBaseName) { - project.task("fixTrampolineFilenames${variantBaseName}").configure({ - }).doFirst { - ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"].each { arch -> - def initial = new File( - "${buildDir}/intermediates/ndkBuild/${variantBaseName}/obj/local/${arch}/crashlytics-trampoline") - def renamed = new File( - "${buildDir}/intermediates/ndkBuild/${variantBaseName}/obj/local/${arch}/libcrashlytics-trampoline.so") - - // There is no need to delete the original file, it will not be - // packaged into the APK - Files.copy(initial.toPath(), renamed.toPath(), StandardCopyOption.REPLACE_EXISTING) + tasks.findAll { + it.name.startsWith("externalNativeBuild") && !it.name.contains("Clean") + }.each { task -> + fixTasks.each { fix -> fix.configure { it.dependsOn task } } } } } diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties index d7552b04cd2..da937e12fe4 100644 --- a/firebase-crashlytics-ndk/gradle.properties +++ b/firebase-crashlytics-ndk/gradle.properties @@ -1,2 +1,2 @@ -version=18.2.14 -latestReleasedVersion=18.2.13 +version=18.3.3 +latestReleasedVersion=18.3.2 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index d356950122f..473ce7ab9ff 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,6 +1,31 @@ # Unreleased +# 18.3.2 +* [unchanged] Updated to accommodate the release of the updated + `firebase-crashlytics-ndk` v18.3.2. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + +# 18.3.1 +* [fixed] Fixed an [issue](https://github.com/firebase/firebase-android-sdk/issues/4223){: .external} + in v18.3.0 that caused a `NoClassDefFoundError` in specific cases. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + # 18.3.0 +Warning: We're aware of an +[issue](https://github.com/firebase/firebase-android-sdk/issues/4223){: .external} +in this version of the [crashlytics] Android SDK.
**We strongly recommend +using the latest version of the SDK (v18.3.1+ or [bom] v31.0.1+).** + * [changed] Improved crash reporting reliability for crashes that occur early in the app's lifecycle. diff --git a/firebase-crashlytics/firebase-crashlytics.gradle b/firebase-crashlytics/firebase-crashlytics.gradle index a63495c83c7..7974448fd26 100644 --- a/firebase-crashlytics/firebase-crashlytics.gradle +++ b/firebase-crashlytics/firebase-crashlytics.gradle @@ -92,5 +92,5 @@ dependencies { androidTestImplementation "com.google.truth:truth:$googleTruthVersion" androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.28.1' androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.28.1' - androidTestImplementation 'com.google.protobuf:protobuf-java:3.14.0' + androidTestImplementation 'com.google.protobuf:protobuf-java:3.21.9' } diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties index d7552b04cd2..da937e12fe4 100644 --- a/firebase-crashlytics/gradle.properties +++ b/firebase-crashlytics/gradle.properties @@ -1,2 +1,2 @@ -version=18.2.14 -latestReleasedVersion=18.2.13 +version=18.3.3 +latestReleasedVersion=18.3.2 diff --git a/firebase-crashlytics/ktx/src/androidTest/kotlin/com/google/firebase/crashlytics/ktx/CrashlyticsTests.kt b/firebase-crashlytics/ktx/src/androidTest/kotlin/com/google/firebase/crashlytics/ktx/CrashlyticsTests.kt index 383a785ed43..296f893d158 100644 --- a/firebase-crashlytics/ktx/src/androidTest/kotlin/com/google/firebase/crashlytics/ktx/CrashlyticsTests.kt +++ b/firebase-crashlytics/ktx/src/androidTest/kotlin/com/google/firebase/crashlytics/ktx/CrashlyticsTests.kt @@ -30,46 +30,54 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class CrashlyticsTests { - companion object { - lateinit var app: FirebaseApp + companion object { + lateinit var app: FirebaseApp - @BeforeClass @JvmStatic fun setup() { - app = Firebase.initialize(InstrumentationRegistry.getContext())!! - } - - @AfterClass @JvmStatic fun cleanup() { - app.delete() - } + @BeforeClass + @JvmStatic + fun setup() { + app = Firebase.initialize(InstrumentationRegistry.getContext())!! } - @Test - fun firebaseCrashlyticsDelegates() { - assertThat(Firebase.crashlytics).isSameInstanceAs(FirebaseCrashlytics.getInstance()) + @AfterClass + @JvmStatic + fun cleanup() { + app.delete() } + } - @Test - fun testDataCall() { - assertThat("hola").isEqualTo("hola") - } + @Test + fun firebaseCrashlyticsDelegates() { + assertThat(Firebase.crashlytics).isSameInstanceAs(FirebaseCrashlytics.getInstance()) + } + + @Test + fun testDataCall() { + assertThat("hola").isEqualTo("hola") + } } @RunWith(AndroidJUnit4::class) class LibraryVersionTest { - companion object { - lateinit var app: FirebaseApp + companion object { + lateinit var app: FirebaseApp - @BeforeClass @JvmStatic fun setup() { - app = Firebase.initialize(InstrumentationRegistry.getContext())!! - } - - @AfterClass @JvmStatic fun cleanup() { - app.delete() - } + @BeforeClass + @JvmStatic + fun setup() { + app = Firebase.initialize(InstrumentationRegistry.getContext())!! } - @Test - fun libraryRegistrationAtRuntime() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) + @AfterClass + @JvmStatic + fun cleanup() { + app.delete() } + } + + @Test + fun libraryRegistrationAtRuntime() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt b/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt index b16ca8f569b..46d7bad3375 100644 --- a/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt +++ b/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt @@ -24,14 +24,12 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseCrashlytics] instance of the default [FirebaseApp]. */ val Firebase.crashlytics: FirebaseCrashlytics - get() = FirebaseCrashlytics.getInstance() + get() = FirebaseCrashlytics.getInstance() -/** - * Associates all key-value parameters with the reports - */ +/** Associates all key-value parameters with the reports */ fun FirebaseCrashlytics.setCustomKeys(init: KeyValueBuilder.() -> Unit) { - val builder = KeyValueBuilder(this) - builder.init() + val builder = KeyValueBuilder(this) + builder.init() } internal const val LIBRARY_NAME: String = "fire-cls-ktx" @@ -39,6 +37,6 @@ internal const val LIBRARY_NAME: String = "fire-cls-ktx" /** @suppress */ @Keep class FirebaseCrashlyticsKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt b/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt index 05d504e9f4b..3d1f5bfd9ad 100644 --- a/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt +++ b/firebase-crashlytics/ktx/src/main/kotlin/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt @@ -17,25 +17,23 @@ package com.google.firebase.crashlytics.ktx import com.google.firebase.crashlytics.FirebaseCrashlytics /** Helper class to enable fluent syntax in [setCustomKeys] */ -class KeyValueBuilder( - private val crashlytics: FirebaseCrashlytics -) { +class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: Boolean) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: Boolean) = crashlytics.setCustomKey(key, value) - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: Double) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: Double) = crashlytics.setCustomKey(key, value) - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: Float) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: Float) = crashlytics.setCustomKey(key, value) - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: Int) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: Int) = crashlytics.setCustomKey(key, value) - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: Long) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: Long) = crashlytics.setCustomKey(key, value) - /** Sets a custom key and value that are associated with reports. */ - fun key(key: String, value: String) = crashlytics.setCustomKey(key, value) + /** Sets a custom key and value that are associated with reports. */ + fun key(key: String, value: String) = crashlytics.setCustomKey(key, value) } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index 4c4ecad1795..c3027ff690f 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.crashlytics.internal.common; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ApplicationExitInfo; import android.content.Context; @@ -272,6 +273,8 @@ private Task waitForReportAction() { // If data collection gets enabled while we are waiting for an action, go ahead and send the // reports, and any subsequent explicit response will be ignored. + // TODO(b/261014167): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") final Task collectionEnabled = dataCollectionArbiter .waitForAutomaticDataCollectionEnabled() @@ -327,6 +330,8 @@ Task deleteUnsentReports() { return unsentReportsHandled.getTask(); } + // TODO(b/261014167): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") Task submitAllReports(Task settingsDataTask) { if (!reportingCoordinator.hasReportsToSend()) { // Just notify the user that there are no reports and stop. @@ -734,6 +739,9 @@ private Task logAnalyticsAppExceptionEvent(long timestamp) { return Tasks.forResult(null); } Logger.getLogger().d("Logging app exception event to Firebase Analytics"); + + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") final ThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); return Tasks.call( executor, diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/ExecutorUtils.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/ExecutorUtils.java index 9147921af73..77fafe9adc5 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/ExecutorUtils.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/ExecutorUtils.java @@ -16,6 +16,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; +import android.annotation.SuppressLint; import com.google.firebase.crashlytics.internal.Logger; import java.util.Locale; import java.util.concurrent.ExecutorService; @@ -43,6 +44,8 @@ public static ExecutorService buildSingleThreadExecutorService(String name) { public static ScheduledExecutorService buildSingleThreadScheduledExecutorService(String name) { final ThreadFactory threadFactory = ExecutorUtils.getNamedThreadFactory(name); + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(threadFactory); ExecutorUtils.addDelayedShutdownHook(name, executor); @@ -70,6 +73,8 @@ public void onRun() { }; } + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static ExecutorService newSingleThreadExecutor( ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { return Executors.unconfigurableExecutorService( @@ -88,11 +93,14 @@ private static void addDelayedShutdownHook(String serviceName, ExecutorService s serviceName, service, DEFAULT_TERMINATION_TIMEOUT, SECONDS); } + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static void addDelayedShutdownHook( final String serviceName, final ExecutorService service, final long terminationTimeout, final TimeUnit timeUnit) { + Runtime.getRuntime() .addShutdownHook( new Thread( diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/Utils.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/Utils.java index 9c752158cdf..76278620add 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/Utils.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/Utils.java @@ -16,6 +16,7 @@ import static java.util.Objects.requireNonNull; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; @@ -34,6 +35,8 @@ public final class Utils { private Utils() {} /** @return A tasks that is resolved when either of the given tasks is resolved. */ + // TODO(b/261014167): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public static Task race(Task t1, Task t2) { final TaskCompletionSource result = new TaskCompletionSource<>(); Continuation continuation = @@ -72,6 +75,8 @@ public static Task callTask(Executor executor, Callable> callable final TaskCompletionSource tcs = new TaskCompletionSource(); executor.execute( new Runnable() { + // TODO(b/261014167): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @Override public void run() { try { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/ReportQueue.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/ReportQueue.java index d25c86c0eff..39c79d07c3c 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/ReportQueue.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/send/ReportQueue.java @@ -62,6 +62,8 @@ final class ReportQueue { onDemandCounter); } + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") ReportQueue( double ratePerMinute, double base, @@ -120,7 +122,8 @@ TaskCompletionSource enqueueReport( } } - @SuppressLint("DiscouragedApi") // best effort only + // TODO(b/258263226): Migrate to go/firebase-android-executors + @SuppressLint({"DiscouragedApi", "ThreadPoolCreation"}) // best effort only public void flushScheduledReportsIfAble() { CountDownLatch latch = new CountDownLatch(1); new Thread( diff --git a/firebase-database/firebase-database.gradle b/firebase-database/firebase-database.gradle index 37231be536e..f19c95f4d3f 100644 --- a/firebase-database/firebase-database.gradle +++ b/firebase-database/firebase-database.gradle @@ -100,7 +100,6 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation 'org.codehaus.plexus:plexus-utils:3.4.2' testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation 'com.firebase:firebase-token-generator:2.0.0' testImplementation 'com.fasterxml.jackson.core:jackson-core:2.9.8' diff --git a/firebase-database/gradle.properties b/firebase-database/gradle.properties index b5000bdda31..2485d5054e8 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=20.0.7 -latestReleasedVersion=20.0.6 +version=20.1.1 +latestReleasedVersion=20.1.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/ChildEvent.kt b/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/ChildEvent.kt index aeadeec5f39..95cb8e18ac2 100644 --- a/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/ChildEvent.kt +++ b/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/ChildEvent.kt @@ -1,43 +1,63 @@ +// Copyright 2022 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.database.ktx import com.google.firebase.database.DataSnapshot /** - * Used to emit events about changes in the child locations of a given - * [Query] when using the [childEvents] Flow. + * Used to emit events about changes in the child locations of a given [Query] when using the + * [childEvents] Flow. */ sealed class ChildEvent { - /** - * Emitted when a new child is added to the location. - * - * @param snapshot An immutable snapshot of the data at the new child location - * @param previousChildName The key name of sibling location ordered before the new child. This - * will be null for the first child node of a location. - */ - data class Added(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() + /** + * Emitted when a new child is added to the location. + * + * @param snapshot An immutable snapshot of the data at the new child location + * @param previousChildName The key name of sibling location ordered before the new child. This + * ``` + * will be null for the first child node of a location. + * ``` + */ + data class Added(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() - /** - * Emitted when the data at a child location has changed. - * - * @param snapshot An immutable snapshot of the data at the new data at the child location - * @param previousChildName The key name of sibling location ordered before the child. This will - * be null for the first child node of a location. - */ - data class Changed(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() + /** + * Emitted when the data at a child location has changed. + * + * @param snapshot An immutable snapshot of the data at the new data at the child location + * @param previousChildName The key name of sibling location ordered before the child. This will + * ``` + * be null for the first child node of a location. + * ``` + */ + data class Changed(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() - /** - * Emitted when a child is removed from the location. - * - * @param snapshot An immutable snapshot of the data at the child that was removed. - */ - data class Removed(val snapshot: DataSnapshot) : ChildEvent() + /** + * Emitted when a child is removed from the location. + * + * @param snapshot An immutable snapshot of the data at the child that was removed. + */ + data class Removed(val snapshot: DataSnapshot) : ChildEvent() - /** - * Emitted when a child location's priority changes. - * - * @param snapshot An immutable snapshot of the data at the location that moved. - * @param previousChildName The key name of the sibling location ordered before the child - * location. This will be null if this location is ordered first. - */ - data class Moved(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() + /** + * Emitted when a child location's priority changes. + * + * @param snapshot An immutable snapshot of the data at the location that moved. + * @param previousChildName The key name of the sibling location ordered before the child + * ``` + * location. This will be null if this location is ordered first. + * ``` + */ + data class Moved(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() } diff --git a/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/Database.kt b/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/Database.kt index b200948bf49..dfe6c0d23ce 100644 --- a/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/Database.kt +++ b/firebase-database/ktx/src/main/kotlin/com/google/firebase/database/ktx/Database.kt @@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.callbackFlow /** Returns the [FirebaseDatabase] instance of the default [FirebaseApp]. */ val Firebase.database: FirebaseDatabase - get() = FirebaseDatabase.getInstance() + get() = FirebaseDatabase.getInstance() /** Returns the [FirebaseDatabase] instance for the specified [url]. */ fun Firebase.database(url: String): FirebaseDatabase = FirebaseDatabase.getInstance(url) @@ -45,26 +45,26 @@ fun Firebase.database(app: FirebaseApp): FirebaseDatabase = FirebaseDatabase.get /** Returns the [FirebaseDatabase] instance of the given [FirebaseApp] and [url]. */ fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = -FirebaseDatabase.getInstance(app, url) + FirebaseDatabase.getInstance(app, url) /** * Returns the content of the DataSnapshot converted to a POJO. * - * Supports generics like List<> or Map<>. Use @JvmSuppressWildcards to force the compiler to - * use the type `T`, and not `? extends T`. + * Supports generics like List<> or Map<>. Use @JvmSuppressWildcards to force the compiler to use + * the type `T`, and not `? extends T`. */ inline fun DataSnapshot.getValue(): T? { - return getValue(object : GenericTypeIndicator() {}) + return getValue(object : GenericTypeIndicator() {}) } /** * Returns the content of the MutableData converted to a POJO. * - * Supports generics like List<> or Map<>. Use @JvmSuppressWildcards to force the compiler to - * use the type `T`, and not `? extends T`. + * Supports generics like List<> or Map<>. Use @JvmSuppressWildcards to force the compiler to use + * the type `T`, and not `? extends T`. */ inline fun MutableData.getValue(): T? { - return getValue(object : GenericTypeIndicator() {}) + return getValue(object : GenericTypeIndicator() {}) } /** @@ -74,19 +74,21 @@ inline fun MutableData.getValue(): T? { * - When the flow completes, the listener will be removed. */ val Query.snapshots - get() = callbackFlow { - val listener = addValueEventListener(object : ValueEventListener { + get() = + callbackFlow { + val listener = + addValueEventListener( + object : ValueEventListener { override fun onDataChange(snapshot: DataSnapshot) { - repo.scheduleNow { - trySendBlocking(snapshot) - } + repo.scheduleNow { trySendBlocking(snapshot) } } override fun onCancelled(error: DatabaseError) { - cancel(message = "Error getting Query snapshot", cause = error.toException()) + cancel(message = "Error getting Query snapshot", cause = error.toException()) } - }) - awaitClose { removeEventListener(listener) } + } + ) + awaitClose { removeEventListener(listener) } } /** @@ -96,37 +98,33 @@ val Query.snapshots * - When the flow completes, the listener will be removed. */ val Query.childEvents - get() = callbackFlow { - val listener = addChildEventListener(object : ChildEventListener { + get() = + callbackFlow { + val listener = + addChildEventListener( + object : ChildEventListener { override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) { - repo.scheduleNow { - trySendBlocking(ChildEvent.Added(snapshot, previousChildName)) - } + repo.scheduleNow { trySendBlocking(ChildEvent.Added(snapshot, previousChildName)) } } override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) { - repo.scheduleNow { - trySendBlocking(ChildEvent.Changed(snapshot, previousChildName)) - } + repo.scheduleNow { trySendBlocking(ChildEvent.Changed(snapshot, previousChildName)) } } override fun onChildRemoved(snapshot: DataSnapshot) { - repo.scheduleNow { - trySendBlocking(ChildEvent.Removed(snapshot)) - } + repo.scheduleNow { trySendBlocking(ChildEvent.Removed(snapshot)) } } override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) { - repo.scheduleNow { - trySendBlocking(ChildEvent.Moved(snapshot, previousChildName)) - } + repo.scheduleNow { trySendBlocking(ChildEvent.Moved(snapshot, previousChildName)) } } override fun onCancelled(error: DatabaseError) { - cancel(message = "Error getting Query childEvent", cause = error.toException()) + cancel(message = "Error getting Query childEvent", cause = error.toException()) } - }) - awaitClose { removeEventListener(listener) } + } + ) + awaitClose { removeEventListener(listener) } } internal const val LIBRARY_NAME: String = "fire-db-ktx" @@ -134,6 +132,6 @@ internal const val LIBRARY_NAME: String = "fire-db-ktx" /** @suppress */ @Keep class FirebaseDatabaseKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/DataSnapshotUtil.kt b/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/DataSnapshotUtil.kt index 0890fe497e1..4d940c82687 100644 --- a/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/DataSnapshotUtil.kt +++ b/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/DataSnapshotUtil.kt @@ -20,22 +20,22 @@ import com.google.firebase.database.snapshot.NodeUtilities /** * Creates a custom DataSnapshot. * - * This method is a workaround that enables the creation of a custom - * DataSnapshot using package-private methods. + * This method is a workaround that enables the creation of a custom DataSnapshot using + * package-private methods. */ fun createDataSnapshot(data: Any?, db: FirebaseDatabase): DataSnapshot { - var ref = DatabaseReference("https://test.firebaseio.com", db.config) - val node = NodeUtilities.NodeFromJSON(data) - return DataSnapshot(ref, IndexedNode.from(node)) + var ref = DatabaseReference("https://test.firebaseio.com", db.config) + val node = NodeUtilities.NodeFromJSON(data) + return DataSnapshot(ref, IndexedNode.from(node)) } /** * Creates a custom MutableData. * - * This method is a workaround that enables the creation of a custom - * MutableData using package-private methods. + * This method is a workaround that enables the creation of a custom MutableData using + * package-private methods. */ fun createMutableData(data: Any?): MutableData { - val node = NodeUtilities.NodeFromJSON(data) - return MutableData(node) + val node = NodeUtilities.NodeFromJSON(data) + return MutableData(node) } diff --git a/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/ktx/DatabaseTests.kt b/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/ktx/DatabaseTests.kt index 3860d2c93e7..6eedd93d385 100644 --- a/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/ktx/DatabaseTests.kt +++ b/firebase-database/ktx/src/test/kotlin/com/google/firebase/database/ktx/DatabaseTests.kt @@ -40,194 +40,178 @@ const val EXISTING_APP = "existing" @IgnoreExtraProperties data class Player( - var name: String? = "", - var jersey: Int? = -1, - var goalkeeper: Boolean? = false, - var avg_goals_per_game: Double? = 0.0 + var name: String? = "", + var jersey: Int? = -1, + var goalkeeper: Boolean? = false, + var avg_goals_per_game: Double? = 0.0 ) { - @Exclude - fun toMap(): Map { - return mapOf( - "name" to name, - "jersey" to jersey, - "goalkeeper" to goalkeeper, - "avg_goals_per_game" to avg_goals_per_game - ) - } + @Exclude + fun toMap(): Map { + return mapOf( + "name" to name, + "jersey" to jersey, + "goalkeeper" to goalkeeper, + "avg_goals_per_game" to avg_goals_per_game + ) + } } abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .setDatabaseUrl("http://tests.fblocal.com:9000") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .setDatabaseUrl("http://tests.fblocal.com:9000") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .setDatabaseUrl("http://tests.fblocal.com:9000") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .setDatabaseUrl("http://tests.fblocal.com:9000") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class DatabaseTests : BaseTestCase() { - @Test - fun `database should delegate to FirebaseDatabase#getInstance()`() { - assertThat(Firebase.database).isSameInstanceAs(FirebaseDatabase.getInstance()) - } - - @Test - fun `FirebaseApp#database should delegate to FirebaseDatabase#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.database(app)).isSameInstanceAs(FirebaseDatabase.getInstance(app)) - } - - @Test - fun `Firebase#database should delegate to FirebaseDatabase#getInstance(url)`() { - val url = "http://tests.fblocal.com:9000" - assertThat(Firebase.database(url)).isSameInstanceAs(FirebaseDatabase.getInstance(url)) - } - - @Test - fun `Firebase#database should delegate to FirebaseDatabase#getInstance(FirebaseApp, url)`() { - val app = Firebase.app(EXISTING_APP) - val url = "http://tests.fblocal.com:9000" - assertThat(Firebase.database(app, url)).isSameInstanceAs(FirebaseDatabase.getInstance(app, url)) - } + @Test + fun `database should delegate to FirebaseDatabase#getInstance()`() { + assertThat(Firebase.database).isSameInstanceAs(FirebaseDatabase.getInstance()) + } + + @Test + fun `FirebaseApp#database should delegate to FirebaseDatabase#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.database(app)).isSameInstanceAs(FirebaseDatabase.getInstance(app)) + } + + @Test + fun `Firebase#database should delegate to FirebaseDatabase#getInstance(url)`() { + val url = "http://tests.fblocal.com:9000" + assertThat(Firebase.database(url)).isSameInstanceAs(FirebaseDatabase.getInstance(url)) + } + + @Test + fun `Firebase#database should delegate to FirebaseDatabase#getInstance(FirebaseApp, url)`() { + val app = Firebase.app(EXISTING_APP) + val url = "http://tests.fblocal.com:9000" + assertThat(Firebase.database(app, url)).isSameInstanceAs(FirebaseDatabase.getInstance(app, url)) + } } @RunWith(RobolectricTestRunner::class) class DataSnapshotTests : BaseTestCase() { - @Test - fun `reified getValue works with basic types`() { - val data = mapOf( - "name" to "John Doe", - "jersey" to 35L, - "goalkeeper" to false, - "avg_goals_per_game" to 0.35 - ) - val dataSnapshot = createDataSnapshot(data, Firebase.database) - assertThat(dataSnapshot.child("name").getValue()).isEqualTo("John Doe") - assertThat(dataSnapshot.child("jersey").getValue()).isEqualTo(35L) - assertThat(dataSnapshot.child("goalkeeper").getValue()).isEqualTo(false) - assertThat(dataSnapshot.child("avg_goals_per_game").getValue()).isEqualTo(0.35) - } - - @Test - fun `reified getValue works with maps`() { - val data = mapOf( - "name" to "John Doe", - "jersey" to 35L, - "goalkeeper" to false, - "avg_goals_per_game" to 0.35 - ) - val dataSnapshot = createDataSnapshot(data, Firebase.database) - assertThat(dataSnapshot.getValue>()).isEqualTo(data) - } - - @Test - fun `reified getValue works with lists types`() { - val data = listOf( - "George", - "John", - "Paul", - "Ringo" - ) - val dataSnapshot = createDataSnapshot(data, Firebase.database) - assertThat(dataSnapshot.getValue>()).isEqualTo(data) - } - - @Test - fun `reified getValue works with custom types`() { - val data = Player( - name = "John Doe", - jersey = 35, - goalkeeper = false, - avg_goals_per_game = 0.35 - ) - val dataSnapshot = createDataSnapshot(data.toMap(), Firebase.database) - assertThat(dataSnapshot.getValue()).isEqualTo(data) - } + @Test + fun `reified getValue works with basic types`() { + val data = + mapOf( + "name" to "John Doe", + "jersey" to 35L, + "goalkeeper" to false, + "avg_goals_per_game" to 0.35 + ) + val dataSnapshot = createDataSnapshot(data, Firebase.database) + assertThat(dataSnapshot.child("name").getValue()).isEqualTo("John Doe") + assertThat(dataSnapshot.child("jersey").getValue()).isEqualTo(35L) + assertThat(dataSnapshot.child("goalkeeper").getValue()).isEqualTo(false) + assertThat(dataSnapshot.child("avg_goals_per_game").getValue()).isEqualTo(0.35) + } + + @Test + fun `reified getValue works with maps`() { + val data = + mapOf( + "name" to "John Doe", + "jersey" to 35L, + "goalkeeper" to false, + "avg_goals_per_game" to 0.35 + ) + val dataSnapshot = createDataSnapshot(data, Firebase.database) + assertThat(dataSnapshot.getValue>()).isEqualTo(data) + } + + @Test + fun `reified getValue works with lists types`() { + val data = listOf("George", "John", "Paul", "Ringo") + val dataSnapshot = createDataSnapshot(data, Firebase.database) + assertThat(dataSnapshot.getValue>()).isEqualTo(data) + } + + @Test + fun `reified getValue works with custom types`() { + val data = Player(name = "John Doe", jersey = 35, goalkeeper = false, avg_goals_per_game = 0.35) + val dataSnapshot = createDataSnapshot(data.toMap(), Firebase.database) + assertThat(dataSnapshot.getValue()).isEqualTo(data) + } } @RunWith(RobolectricTestRunner::class) class MutableDataTests : BaseTestCase() { - @Test - fun `reified getValue works with basic types`() { - val data = mapOf( - "name" to "John Doe", - "jersey" to 35L, - "goalkeeper" to false, - "avg_goals_per_game" to 0.35 - ) - val mutableData = createMutableData(data) - - assertThat(mutableData.child("name").getValue()).isEqualTo("John Doe") - assertThat(mutableData.child("jersey").getValue()).isEqualTo(35L) - assertThat(mutableData.child("goalkeeper").getValue()).isEqualTo(false) - assertThat(mutableData.child("avg_goals_per_game").getValue()).isEqualTo(0.35) - } - - @Test - fun `reified getValue works with maps`() { - val data = mapOf( - "name" to "John Doe", - "jersey" to 35L, - "goalkeeper" to false, - "avg_goals_per_game" to 0.35 - ) - val mutableData = createMutableData(data) - assertThat(mutableData.getValue>()).isEqualTo(data) - } - - @Test - fun `reified getValue works with lists types`() { - val data = listOf( - "George", - "John", - "Paul", - "Ringo" - ) - val mutableData = createMutableData(data) - assertThat(mutableData.getValue>()).isEqualTo(data) - } - - @Test - fun `reified getValue works with custom types`() { - val data = Player( - name = "John Doe", - jersey = 35, - goalkeeper = false, - avg_goals_per_game = 0.35 - ) - val mutableData = createMutableData(data.toMap()) - assertThat(mutableData.getValue()).isEqualTo(data) - } + @Test + fun `reified getValue works with basic types`() { + val data = + mapOf( + "name" to "John Doe", + "jersey" to 35L, + "goalkeeper" to false, + "avg_goals_per_game" to 0.35 + ) + val mutableData = createMutableData(data) + + assertThat(mutableData.child("name").getValue()).isEqualTo("John Doe") + assertThat(mutableData.child("jersey").getValue()).isEqualTo(35L) + assertThat(mutableData.child("goalkeeper").getValue()).isEqualTo(false) + assertThat(mutableData.child("avg_goals_per_game").getValue()).isEqualTo(0.35) + } + + @Test + fun `reified getValue works with maps`() { + val data = + mapOf( + "name" to "John Doe", + "jersey" to 35L, + "goalkeeper" to false, + "avg_goals_per_game" to 0.35 + ) + val mutableData = createMutableData(data) + assertThat(mutableData.getValue>()).isEqualTo(data) + } + + @Test + fun `reified getValue works with lists types`() { + val data = listOf("George", "John", "Paul", "Ringo") + val mutableData = createMutableData(data) + assertThat(mutableData.getValue>()).isEqualTo(data) + } + + @Test + fun `reified getValue works with custom types`() { + val data = Player(name = "John Doe", jersey = 35, goalkeeper = false, avg_goals_per_game = 0.35) + val mutableData = createMutableData(data.toMap()) + assertThat(mutableData.getValue()).isEqualTo(data) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-database/src/main/java/com/google/firebase/database/DataSnapshot.java b/firebase-database/src/main/java/com/google/firebase/database/DataSnapshot.java index 11db5a93b2f..4de53729ac9 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/DataSnapshot.java +++ b/firebase-database/src/main/java/com/google/firebase/database/DataSnapshot.java @@ -105,12 +105,12 @@ public boolean exists() { * returned are: * *

    - *
  • Boolean - *
  • String - *
  • Long - *
  • Double - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • String + *
  • Long + *
  • Double + *
  • Map<String, Object> + *
  • List<Object> *
* * This list is recursive; the possible types for {@link java.lang.Object} in the above list is @@ -129,12 +129,12 @@ public Object getValue() { * returned are: * *
    - *
  • Boolean - *
  • String - *
  • Long - *
  • Double - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • String + *
  • Long + *
  • Double + *
  • Map<String, Object> + *
  • List<Object> *
* * This list is recursive; the possible types for {@link java.lang.Object} in the above list is @@ -166,7 +166,7 @@ public Object getValue(boolean useExportFormat) { * * An example class might look like: * - *

+   * 
    *     class Message {
    *         private String author;
    *         private String text;
@@ -190,7 +190,7 @@ public Object getValue(boolean useExportFormat) {
    *
    *     // Later
    *     Message m = snapshot.getValue(Message.class);
-   * 
+ *
* * @param valueType The class into which this snapshot should be marshalled * @param The type to return. Implicitly defined from the class passed in @@ -208,10 +208,10 @@ public T getValue(@NonNull Class valueType) { * properly-typed Collection. So, in the case where you want a {@link java.util.List} of Message * instances, you will need to do something like the following: * - *

+   * 
    *     GenericTypeIndicator<List<Message>> t = new GenericTypeIndicator<List<Message>>() {};
    *     List<Message> messages = snapshot.getValue(t);
-   * 
+ *
* * It is important to use a subclass of {@link GenericTypeIndicator}. See {@link * GenericTypeIndicator} for more details @@ -255,11 +255,13 @@ public String getKey() { /** * Gives access to all of the immediate children of this snapshot. Can be used in native for - * loops: - *
for (DataSnapshot child : parent.getChildren()) { - *
    ... - *
} - *
+ * loops: + * + *
+   * for (DataSnapshot child : parent.getChildren()) {
+   *       ...
+   * }
+   * 
* * @return The immediate children of this snapshot */ @@ -298,8 +300,8 @@ public void remove() { * types: * *
    - *
  • Double - *
  • String + *
  • Double + *
  • String *
* * Note that null is also allowed diff --git a/firebase-database/src/main/java/com/google/firebase/database/DatabaseReference.java b/firebase-database/src/main/java/com/google/firebase/database/DatabaseReference.java index 85e511041cc..c9db620a8ac 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/DatabaseReference.java +++ b/firebase-database/src/main/java/com/google/firebase/database/DatabaseReference.java @@ -126,12 +126,12 @@ public DatabaseReference push() { * correspond to the JSON types: * *
    - *
  • Boolean - *
  • Long - *
  • Double - *
  • String - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • String + *
  • Map<String, Object> + *
  • List<Object> *
* *
@@ -165,12 +165,12 @@ public Task setValue(@Nullable Object value) { * the JSON types: * *
    - *
  • Boolean - *
  • Long - *
  • Double - *
  • String - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • String + *
  • Map<String, Object> + *
  • List<Object> *
* *
@@ -205,12 +205,12 @@ public Task setValue(@Nullable Object value, @Nullable Object priority) { * correspond to the JSON types: * *
    - *
  • Boolean - *
  • Long - *
  • Double - *
  • String - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • String + *
  • Map<String, Object> + *
  • List<Object> *
* *
@@ -242,12 +242,12 @@ public void setValue(@Nullable Object value, @Nullable CompletionListener listen * value correspond to the JSON types: * *
    - *
  • Boolean - *
  • Long - *
  • Double - *
  • String - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • String + *
  • Map<String, Object> + *
  • List<Object> *
* *
diff --git a/firebase-database/src/main/java/com/google/firebase/database/GenericTypeIndicator.java b/firebase-database/src/main/java/com/google/firebase/database/GenericTypeIndicator.java index 0d6d099e6e4..ea447861a7b 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/GenericTypeIndicator.java +++ b/firebase-database/src/main/java/com/google/firebase/database/GenericTypeIndicator.java @@ -25,7 +25,7 @@ * {@link DataSnapshot}:
*
* - *

+ * 
  *     class Message {
  *         private String author;
  *         private String text;
@@ -51,7 +51,7 @@
  *     GenericTypeIndicator<List<Message>> t = new GenericTypeIndicator<List<Message>>() {};
  *     List<Message> messages = snapshot.getValue(t);
  *
- * 
+ *
* * @param The type of generic collection that this instance servers as an indicator for */ diff --git a/firebase-database/src/main/java/com/google/firebase/database/MutableData.java b/firebase-database/src/main/java/com/google/firebase/database/MutableData.java index fda1c2a7c7a..49244e66907 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/MutableData.java +++ b/firebase-database/src/main/java/com/google/firebase/database/MutableData.java @@ -91,11 +91,13 @@ public long getChildrenCount() { } /** - * Used to iterate over the immediate children at this location - *
for (MutableData child : parent.getChildren()) { - *
    ... - *
} - *
+ * Used to iterate over the immediate children at this location + * + *
+   *     for (MutableData child : parent.getChildren()) {
+   *             ...
+   *     }
+   * 
* * @return The immediate children at this location */ @@ -164,12 +166,12 @@ public String getKey() { * returned are: * *
    - *
  • Boolean - *
  • String - *
  • Long - *
  • Double - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • String + *
  • Map<String, Object> + *
  • List<Object> *
* * This list is recursive; the possible types for {@link java.lang.Object} in the above list is @@ -196,7 +198,7 @@ public Object getValue() { * * An example class might look like: * - *

+   * 
    *     class Message {
    *         private String author;
    *         private String text;
@@ -220,7 +222,7 @@ public Object getValue() {
    *
    *     // Later
    *     Message m = mutableData.getValue(Message.class);
-   * 
+ *
* * @param valueType The class into which this data in this instance should be marshalled * @param The type to return. Implicitly defined from the class passed in @@ -238,11 +240,11 @@ public T getValue(@NonNull Class valueType) { * properly-typed Collection. So, in the case where you want a {@link java.util.List} of Message * instances, you will need to do something like the following: * - *

+   * 
    *     GenericTypeIndicator<List<Message>> t =
    *         new GenericTypeIndicator<List<Message>>() {};
    *     List<Message> messages = mutableData.getValue(t);
-   * 
+ *
* * It is important to use a subclass of {@link GenericTypeIndicator}. See {@link * GenericTypeIndicator} for more details @@ -265,11 +267,11 @@ public T getValue(@NonNull GenericTypeIndicator t) { * the value correspond to the JSON types: * *
    - *
  • Boolean - *
  • Long - *
  • Double - *
  • Map<String, Object> - *
  • List<Object> + *
  • Boolean + *
  • Long + *
  • Double + *
  • Map<String, Object> + *
  • List<Object> *
* *
@@ -315,8 +317,8 @@ public void setPriority(@Nullable Object priority) { * Gets the current priority at this location. The possible return types are: * *
    - *
  • Double - *
  • String + *
  • Double + *
  • String *
* * Note that null is allowed diff --git a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAppCheckTokenProvider.java b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAppCheckTokenProvider.java index dddb403afa1..2e088bf7fbb 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAppCheckTokenProvider.java +++ b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAppCheckTokenProvider.java @@ -14,6 +14,7 @@ package com.google.firebase.database.android; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.firebase.appcheck.AppCheckTokenResult; @@ -36,6 +37,8 @@ public AndroidAppCheckTokenProvider( authProvider -> internalAppCheck.set(authProvider.get())); } + // TODO(b/261014172): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @Override public void getToken(boolean forceRefresh, @NonNull final GetTokenCompletionListener listener) { InternalAppCheckTokenProvider appCheckProvider = internalAppCheck.get(); diff --git a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAuthTokenProvider.java b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAuthTokenProvider.java index 7630b0409dd..c1a93cb8088 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAuthTokenProvider.java +++ b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidAuthTokenProvider.java @@ -14,6 +14,7 @@ package com.google.firebase.database.android; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApiNotAvailableException; @@ -36,6 +37,8 @@ public AndroidAuthTokenProvider(Deferred deferredAuthProvi deferredAuthProvider.whenAvailable(authProvider -> internalAuth.set(authProvider.get())); } + // TODO(b/261014172): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @Override public void getToken(boolean forceRefresh, @NonNull final GetTokenCompletionListener listener) { InternalAuthProvider authProvider = internalAuth.get(); diff --git a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidEventTarget.java b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidEventTarget.java index be033c49e91..e76f1bac9c8 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidEventTarget.java +++ b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidEventTarget.java @@ -14,6 +14,7 @@ package com.google.firebase.database.android; +import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import com.google.firebase.database.core.EventTarget; @@ -21,6 +22,8 @@ public class AndroidEventTarget implements EventTarget { private final Handler handler; + // TODO(b/258277572): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public AndroidEventTarget() { this.handler = new Handler(Looper.getMainLooper()); } diff --git a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidPlatform.java b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidPlatform.java index d4e900dd9e6..1ddd20e4c96 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/android/AndroidPlatform.java +++ b/firebase-database/src/main/java/com/google/firebase/database/android/AndroidPlatform.java @@ -14,6 +14,7 @@ package com.google.firebase.database.android; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.os.Handler; @@ -88,6 +89,8 @@ public void handleException(final Throwable e) { // Rethrow on main thread, so the application will crash // The exception might indicate that there is something seriously wrong and better crash, // than continue run in an undefined state... + // TODO(b/258277572): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") Handler handler = new Handler(applicationContext.getMainLooper()); handler.post( new Runnable() { diff --git a/firebase-database/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java b/firebase-database/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java index 90bd199e496..f7c8b4e5df7 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java +++ b/firebase-database/src/main/java/com/google/firebase/database/core/ThreadPoolEventTarget.java @@ -14,6 +14,7 @@ package com.google.firebase.database.core; +import android.annotation.SuppressLint; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; @@ -25,6 +26,8 @@ class ThreadPoolEventTarget implements EventTarget { private final ThreadPoolExecutor executor; + // TODO(b/258277572): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ThreadPoolEventTarget( final ThreadFactory wrappedFactory, final ThreadInitializer threadInitializer) { int poolSize = 1; diff --git a/firebase-database/src/test/java/com/google/firebase/database/PushIdGeneratorTest.java b/firebase-database/src/test/java/com/google/firebase/database/PushIdGeneratorTest.java index 6feabe4e4c7..d09317f4e18 100644 --- a/firebase-database/src/test/java/com/google/firebase/database/PushIdGeneratorTest.java +++ b/firebase-database/src/test/java/com/google/firebase/database/PushIdGeneratorTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import com.google.firebase.database.core.utilities.PushIdGenerator; -import org.codehaus.plexus.util.StringUtils; import org.junit.Test; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -41,8 +40,7 @@ public void testSuccessorSpecialValue() { PushIdGenerator.successor(String.valueOf(Integer.MAX_VALUE))); assertEquals( MAX_KEY_NAME, - PushIdGenerator.successor( - StringUtils.repeat(Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN))); + PushIdGenerator.successor(repeat(Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN))); } @Test @@ -51,9 +49,7 @@ public void testSuccessorBasic() { assertEquals( "abd", PushIdGenerator.successor( - "abc" - + StringUtils.repeat( - Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN - "abc".length()))); + "abc" + repeat(Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN - "abc".length()))); assertEquals( "abc" + MIN_PUSH_CHAR + MIN_PUSH_CHAR, PushIdGenerator.successor("abc" + MIN_PUSH_CHAR)); } @@ -69,8 +65,18 @@ public void testPredecessorSpecialValue() { @Test public void testPredecessorBasicValue() { assertEquals( - "abb" + StringUtils.repeat(Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN - "abc".length()), + "abb" + repeat(Character.toString(MAX_PUSH_CHAR), MAX_KEY_LEN - "abc".length()), PushIdGenerator.predecessor("abc")); assertEquals("abc", PushIdGenerator.predecessor("abc" + MIN_PUSH_CHAR)); } + + private static String repeat(String str, int repeat) { + // Copied from + // https://github.com/codehaus-plexus/plexus-utils/blob/master/src/main/java/org/codehaus/plexus/util/StringUtils.java. + StringBuilder buffer = new StringBuilder(repeat * str.length()); + for (int i = 0; i < repeat; i++) { + buffer.append(str); + } + return buffer.toString(); + } } diff --git a/firebase-datatransport/gradle.properties b/firebase-datatransport/gradle.properties index 10a76e53f6f..a3fbce1885e 100644 --- a/firebase-datatransport/gradle.properties +++ b/firebase-datatransport/gradle.properties @@ -1,4 +1,4 @@ -version=18.1.7 -latestReleasedVersion=18.1.6 +version=18.1.8 +latestReleasedVersion=18.1.7 android.enableUnitTestBinaryResources=true diff --git a/firebase-dynamic-links/gradle.properties b/firebase-dynamic-links/gradle.properties index bc3efbb0b64..0f91d376021 100644 --- a/firebase-dynamic-links/gradle.properties +++ b/firebase-dynamic-links/gradle.properties @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=21.0.3 -latestReleasedVersion=21.0.2 +version=21.1.1 +latestReleasedVersion=21.1.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-dynamic-links/ktx/src/main/kotlin/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt b/firebase-dynamic-links/ktx/src/main/kotlin/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt index 89c6f625828..40503549163 100644 --- a/firebase-dynamic-links/ktx/src/main/kotlin/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt +++ b/firebase-dynamic-links/ktx/src/main/kotlin/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt @@ -28,94 +28,136 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseDynamicLinks] instance of the default [FirebaseApp]. */ val Firebase.dynamicLinks: FirebaseDynamicLinks - get() = FirebaseDynamicLinks.getInstance() + get() = FirebaseDynamicLinks.getInstance() /** Returns the [FirebaseDynamicLinks] instance of a given [FirebaseApp]. */ fun Firebase.dynamicLinks(app: FirebaseApp): FirebaseDynamicLinks { - return FirebaseDynamicLinks.getInstance(app) + return FirebaseDynamicLinks.getInstance(app) } -/** Creates a [DynamicLink.AndroidParameters] object initialized using the [init] function and sets it to the [DynamicLink.Builder] */ +/** + * Creates a [DynamicLink.AndroidParameters] object initialized using the [init] function and sets + * it to the [DynamicLink.Builder] + */ fun DynamicLink.Builder.androidParameters(init: DynamicLink.AndroidParameters.Builder.() -> Unit) { - val builder = DynamicLink.AndroidParameters.Builder() - builder.init() - setAndroidParameters(builder.build()) + val builder = DynamicLink.AndroidParameters.Builder() + builder.init() + setAndroidParameters(builder.build()) } -/** Creates a [DynamicLink.AndroidParameters] object initialized with the specified [packageName] and using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.androidParameters(packageName: String, init: DynamicLink.AndroidParameters.Builder.() -> Unit) { - val builder = DynamicLink.AndroidParameters.Builder(packageName) - builder.init() - setAndroidParameters(builder.build()) +/** + * Creates a [DynamicLink.AndroidParameters] object initialized with the specified [packageName] and + * using the [init] function and sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.androidParameters( + packageName: String, + init: DynamicLink.AndroidParameters.Builder.() -> Unit +) { + val builder = DynamicLink.AndroidParameters.Builder(packageName) + builder.init() + setAndroidParameters(builder.build()) } -/** Creates a [DynamicLink.IosParameters] object initialized with the specified [bundleId] and using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.iosParameters(bundleId: String, init: DynamicLink.IosParameters.Builder.() -> Unit) { - val builder = DynamicLink.IosParameters.Builder(bundleId) - builder.init() - setIosParameters(builder.build()) +/** + * Creates a [DynamicLink.IosParameters] object initialized with the specified [bundleId] and using + * the [init] function and sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.iosParameters( + bundleId: String, + init: DynamicLink.IosParameters.Builder.() -> Unit +) { + val builder = DynamicLink.IosParameters.Builder(bundleId) + builder.init() + setIosParameters(builder.build()) } -/** Creates a [DynamicLink.GoogleAnalyticsParameters] object initialized using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.googleAnalyticsParameters(init: DynamicLink.GoogleAnalyticsParameters.Builder.() -> Unit) { - val builder = DynamicLink.GoogleAnalyticsParameters.Builder() - builder.init() - setGoogleAnalyticsParameters(builder.build()) +/** + * Creates a [DynamicLink.GoogleAnalyticsParameters] object initialized using the [init] function + * and sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.googleAnalyticsParameters( + init: DynamicLink.GoogleAnalyticsParameters.Builder.() -> Unit +) { + val builder = DynamicLink.GoogleAnalyticsParameters.Builder() + builder.init() + setGoogleAnalyticsParameters(builder.build()) } -/** Creates a [DynamicLink.GoogleAnalyticsParameters] object initialized with the specified - * [source], [medium], [campaign] and using the [init] function and sets it to the [DynamicLink.Builder]. */ +/** + * Creates a [DynamicLink.GoogleAnalyticsParameters] object initialized with the specified [source], + * [medium], [campaign] and using the [init] function and sets it to the [DynamicLink.Builder]. + */ fun DynamicLink.Builder.googleAnalyticsParameters( - source: String, - medium: String, - campaign: String, - init: DynamicLink.GoogleAnalyticsParameters.Builder.() -> Unit + source: String, + medium: String, + campaign: String, + init: DynamicLink.GoogleAnalyticsParameters.Builder.() -> Unit ) { - val builder = DynamicLink.GoogleAnalyticsParameters.Builder(source, medium, campaign) - builder.init() - setGoogleAnalyticsParameters(builder.build()) + val builder = DynamicLink.GoogleAnalyticsParameters.Builder(source, medium, campaign) + builder.init() + setGoogleAnalyticsParameters(builder.build()) } -/** Creates a [DynamicLink.ItunesConnectAnalyticsParameters] object initialized using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.itunesConnectAnalyticsParameters(init: DynamicLink.ItunesConnectAnalyticsParameters.Builder.() -> Unit) { - val builder = DynamicLink.ItunesConnectAnalyticsParameters.Builder() - builder.init() - setItunesConnectAnalyticsParameters(builder.build()) +/** + * Creates a [DynamicLink.ItunesConnectAnalyticsParameters] object initialized using the [init] + * function and sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.itunesConnectAnalyticsParameters( + init: DynamicLink.ItunesConnectAnalyticsParameters.Builder.() -> Unit +) { + val builder = DynamicLink.ItunesConnectAnalyticsParameters.Builder() + builder.init() + setItunesConnectAnalyticsParameters(builder.build()) } -/** Creates a [DynamicLink.SocialMetaTagParameters] object initialized using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.socialMetaTagParameters(init: DynamicLink.SocialMetaTagParameters.Builder.() -> Unit) { - val builder = DynamicLink.SocialMetaTagParameters.Builder() - builder.init() - setSocialMetaTagParameters(builder.build()) +/** + * Creates a [DynamicLink.SocialMetaTagParameters] object initialized using the [init] function and + * sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.socialMetaTagParameters( + init: DynamicLink.SocialMetaTagParameters.Builder.() -> Unit +) { + val builder = DynamicLink.SocialMetaTagParameters.Builder() + builder.init() + setSocialMetaTagParameters(builder.build()) } -/** Creates a [DynamicLink.NavigationInfoParameters] object initialized using the [init] function and sets it to the [DynamicLink.Builder] */ -fun DynamicLink.Builder.navigationInfoParameters(init: DynamicLink.NavigationInfoParameters.Builder.() -> Unit) { - val builder = DynamicLink.NavigationInfoParameters.Builder() - builder.init() - setNavigationInfoParameters(builder.build()) +/** + * Creates a [DynamicLink.NavigationInfoParameters] object initialized using the [init] function and + * sets it to the [DynamicLink.Builder] + */ +fun DynamicLink.Builder.navigationInfoParameters( + init: DynamicLink.NavigationInfoParameters.Builder.() -> Unit +) { + val builder = DynamicLink.NavigationInfoParameters.Builder() + builder.init() + setNavigationInfoParameters(builder.build()) } /** Creates a [DynamicLink] object initialized using the [init] function. */ fun FirebaseDynamicLinks.dynamicLink(init: DynamicLink.Builder.() -> Unit): DynamicLink { - val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() - builder.init() - return builder.buildDynamicLink() + val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() + builder.init() + return builder.buildDynamicLink() } /** Creates a [ShortDynamicLink] object initialized using the [init] function. */ -fun FirebaseDynamicLinks.shortLinkAsync(init: DynamicLink.Builder.() -> Unit): Task { - val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() - builder.init() - return builder.buildShortDynamicLink() +fun FirebaseDynamicLinks.shortLinkAsync( + init: DynamicLink.Builder.() -> Unit +): Task { + val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() + builder.init() + return builder.buildShortDynamicLink() } /** Creates a [ShortDynamicLink] object initialized using the [init] function. */ -fun FirebaseDynamicLinks.shortLinkAsync(suffix: Int, init: DynamicLink.Builder.() -> Unit): Task { - val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() - builder.init() - return builder.buildShortDynamicLink(suffix) +fun FirebaseDynamicLinks.shortLinkAsync( + suffix: Int, + init: DynamicLink.Builder.() -> Unit +): Task { + val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() + builder.init() + return builder.buildShortDynamicLink(suffix) } /** Destructuring declaration for [ShortDynamicLink] to provide shortLink. */ @@ -141,6 +183,6 @@ internal const val LIBRARY_NAME: String = "fire-dl-ktx" /** @suppress */ @Keep class FirebaseDynamicLinksKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-dynamic-links/ktx/src/test/kotlin/com/google/firebase/dynamiclinks/ktx/DynamicLinksTests.kt b/firebase-dynamic-links/ktx/src/test/kotlin/com/google/firebase/dynamiclinks/ktx/DynamicLinksTests.kt index 27c4874553e..d6584747b68 100644 --- a/firebase-dynamic-links/ktx/src/test/kotlin/com/google/firebase/dynamiclinks/ktx/DynamicLinksTests.kt +++ b/firebase-dynamic-links/ktx/src/test/kotlin/com/google/firebase/dynamiclinks/ktx/DynamicLinksTests.kt @@ -29,8 +29,8 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner const val APP_ID = "APP_ID" @@ -39,233 +39,241 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class DynamicLinksTests : BaseTestCase() { - @Test - fun `Firebase#dynamicLinks should delegate to FirebaseDynamicLinks#getInstance()`() { - assertThat(Firebase.dynamicLinks).isSameInstanceAs(FirebaseDynamicLinks.getInstance()) - } - - @Test - fun `Firebase#dynamicLinks should delegate to FirebaseDynamicLinks#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.dynamicLinks(app)) - .isSameInstanceAs(FirebaseDynamicLinks.getInstance(app)) - } - - @Test - fun `Firebase#dynamicLinks#createDynamicLink`() { - val exampleLink = "https://example.com" - val exampleDomainUriPrefix = "https://example.page.link" - - val dynamicLinkKtx = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse(exampleLink) - domainUriPrefix = exampleDomainUriPrefix - } - - val dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink() - .setLink(Uri.parse(exampleLink)) - .setDomainUriPrefix(exampleDomainUriPrefix) - .buildDynamicLink() - - assertThat(dynamicLinkKtx.uri).isEqualTo(dynamicLink.uri) - } - - @Test - fun `androidParameters type-safe builder extension works`() { - val fallbackLink = "https://android.com" - val minVersion = 19 - val packageName = "com.example.android" - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - androidParameters { - minimumVersion = minVersion - fallbackUrl = Uri.parse(fallbackLink) - } - } - - val anotherDynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - androidParameters(packageName) { - minimumVersion = minVersion - fallbackUrl = Uri.parse(fallbackLink) - } + @Test + fun `Firebase#dynamicLinks should delegate to FirebaseDynamicLinks#getInstance()`() { + assertThat(Firebase.dynamicLinks).isSameInstanceAs(FirebaseDynamicLinks.getInstance()) + } + + @Test + fun `Firebase#dynamicLinks should delegate to FirebaseDynamicLinks#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.dynamicLinks(app)).isSameInstanceAs(FirebaseDynamicLinks.getInstance(app)) + } + + @Test + fun `Firebase#dynamicLinks#createDynamicLink`() { + val exampleLink = "https://example.com" + val exampleDomainUriPrefix = "https://example.page.link" + + val dynamicLinkKtx = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse(exampleLink) + domainUriPrefix = exampleDomainUriPrefix + } + + val dynamicLink = + FirebaseDynamicLinks.getInstance() + .createDynamicLink() + .setLink(Uri.parse(exampleLink)) + .setDomainUriPrefix(exampleDomainUriPrefix) + .buildDynamicLink() + + assertThat(dynamicLinkKtx.uri).isEqualTo(dynamicLink.uri) + } + + @Test + fun `androidParameters type-safe builder extension works`() { + val fallbackLink = "https://android.com" + val minVersion = 19 + val packageName = "com.example.android" + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + androidParameters { + minimumVersion = minVersion + fallbackUrl = Uri.parse(fallbackLink) } - - assertThat(dynamicLink.uri.getQueryParameter("amv")?.toInt()).isEqualTo(minVersion) - assertThat(dynamicLink.uri.getQueryParameter("afl")).isEqualTo(fallbackLink) - - assertThat(anotherDynamicLink.uri.getQueryParameter("amv")?.toInt()).isEqualTo(minVersion) - assertThat(anotherDynamicLink.uri.getQueryParameter("afl")).isEqualTo(fallbackLink) - assertThat(anotherDynamicLink.uri.getQueryParameter("apn")).isEqualTo(packageName) - } - - @Test - fun `iosParameters type-safe builder extension works`() { - val iosAppStoreId = "123456789" - val iosMinimumVersion = "1.0.1" - val iosBundleId = "com.example.ios" - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - iosParameters(iosBundleId) { - appStoreId = iosAppStoreId - minimumVersion = iosMinimumVersion - } - } - - assertThat(dynamicLink.uri.getQueryParameter("ibi")).isEqualTo(iosBundleId) - assertThat(dynamicLink.uri.getQueryParameter("imv")).isEqualTo(iosMinimumVersion) - assertThat(dynamicLink.uri.getQueryParameter("isi")).isEqualTo(iosAppStoreId) - } - - @Test - fun `googleAnalyticsParameters type-safe builder extension works`() { - val campaignTerm = "Example Term" - val campaignContent = "Example Content" - val campaignSource = "Twitter" - val campaignMedium = "Social" - val campaignName = "Example Promo" - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - googleAnalyticsParameters(campaignSource, campaignMedium, campaignName) { - term = campaignTerm - content = campaignContent - } + } + + val anotherDynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + androidParameters(packageName) { + minimumVersion = minVersion + fallbackUrl = Uri.parse(fallbackLink) } - - assertThat(dynamicLink.uri.getQueryParameter("utm_content")).isEqualTo(campaignContent) - assertThat(dynamicLink.uri.getQueryParameter("utm_term")).isEqualTo(campaignTerm) - assertThat(dynamicLink.uri.getQueryParameter("utm_source")).isEqualTo(campaignSource) - assertThat(dynamicLink.uri.getQueryParameter("utm_medium")).isEqualTo(campaignMedium) - assertThat(dynamicLink.uri.getQueryParameter("utm_campaign")).isEqualTo(campaignName) - } - - @Test - fun `itunesConnectAnalyticsParameters type-safe builder extension works`() { - val ct = "example-campaign" - val pt = "123456" - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - itunesConnectAnalyticsParameters { - providerToken = pt - campaignToken = ct - } + } + + assertThat(dynamicLink.uri.getQueryParameter("amv")?.toInt()).isEqualTo(minVersion) + assertThat(dynamicLink.uri.getQueryParameter("afl")).isEqualTo(fallbackLink) + + assertThat(anotherDynamicLink.uri.getQueryParameter("amv")?.toInt()).isEqualTo(minVersion) + assertThat(anotherDynamicLink.uri.getQueryParameter("afl")).isEqualTo(fallbackLink) + assertThat(anotherDynamicLink.uri.getQueryParameter("apn")).isEqualTo(packageName) + } + + @Test + fun `iosParameters type-safe builder extension works`() { + val iosAppStoreId = "123456789" + val iosMinimumVersion = "1.0.1" + val iosBundleId = "com.example.ios" + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + iosParameters(iosBundleId) { + appStoreId = iosAppStoreId + minimumVersion = iosMinimumVersion } - - assertThat(dynamicLink.uri.getQueryParameter("pt")).isEqualTo(pt) - assertThat(dynamicLink.uri.getQueryParameter("ct")).isEqualTo(ct) - } - - @Test - fun `socialMetaTagParameters type-safe builder extension works`() { - val socialTitle = "Example Title" - val socialDescription = "This link works whether the app is installed or not!" - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - socialMetaTagParameters { - title = socialTitle - description = socialDescription - } + } + + assertThat(dynamicLink.uri.getQueryParameter("ibi")).isEqualTo(iosBundleId) + assertThat(dynamicLink.uri.getQueryParameter("imv")).isEqualTo(iosMinimumVersion) + assertThat(dynamicLink.uri.getQueryParameter("isi")).isEqualTo(iosAppStoreId) + } + + @Test + fun `googleAnalyticsParameters type-safe builder extension works`() { + val campaignTerm = "Example Term" + val campaignContent = "Example Content" + val campaignSource = "Twitter" + val campaignMedium = "Social" + val campaignName = "Example Promo" + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + googleAnalyticsParameters(campaignSource, campaignMedium, campaignName) { + term = campaignTerm + content = campaignContent } - - assertThat(dynamicLink.uri.getQueryParameter("st")).isEqualTo(socialTitle) - assertThat(dynamicLink.uri.getQueryParameter("sd")).isEqualTo(socialDescription) - } - - @Test - fun `navigationInfoParameters type-safe builder extension works`() { - val forcedRedirect = true - - val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://example.com") - domainUriPrefix = "https://example.page.link" - navigationInfoParameters { - forcedRedirectEnabled = true - } + } + + assertThat(dynamicLink.uri.getQueryParameter("utm_content")).isEqualTo(campaignContent) + assertThat(dynamicLink.uri.getQueryParameter("utm_term")).isEqualTo(campaignTerm) + assertThat(dynamicLink.uri.getQueryParameter("utm_source")).isEqualTo(campaignSource) + assertThat(dynamicLink.uri.getQueryParameter("utm_medium")).isEqualTo(campaignMedium) + assertThat(dynamicLink.uri.getQueryParameter("utm_campaign")).isEqualTo(campaignName) + } + + @Test + fun `itunesConnectAnalyticsParameters type-safe builder extension works`() { + val ct = "example-campaign" + val pt = "123456" + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + itunesConnectAnalyticsParameters { + providerToken = pt + campaignToken = ct } - - val efr = Integer.parseInt(dynamicLink.uri.getQueryParameter("efr")!!) == 1 - assertThat(efr).isEqualTo(forcedRedirect) - } - - @Test - fun `ShortDynamicLink destructure declaration works`() { - val fakeWarning = object : ShortDynamicLink.Warning { - override fun getMessage() = "Warning" - override fun getCode() = "warning" + } + + assertThat(dynamicLink.uri.getQueryParameter("pt")).isEqualTo(pt) + assertThat(dynamicLink.uri.getQueryParameter("ct")).isEqualTo(ct) + } + + @Test + fun `socialMetaTagParameters type-safe builder extension works`() { + val socialTitle = "Example Title" + val socialDescription = "This link works whether the app is installed or not!" + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + socialMetaTagParameters { + title = socialTitle + description = socialDescription } - - val expectedShortLink = Uri.parse("https://example.com") - val expectedPreviewLink = Uri.parse("https://example.com/preview") - val expectedWarnings = mutableListOf(fakeWarning) - - val mockShortDynamicLink = mock(ShortDynamicLink::class.java) - `when`(mockShortDynamicLink.shortLink).thenReturn(expectedShortLink) - `when`(mockShortDynamicLink.previewLink).thenReturn(expectedPreviewLink) - `when`(mockShortDynamicLink.warnings).thenReturn(expectedWarnings) - - val (shortLink, previewLink, warnings) = mockShortDynamicLink - - assertThat(shortLink).isEqualTo(expectedShortLink) - assertThat(previewLink).isEqualTo(expectedPreviewLink) - assertThat(warnings).isEqualTo(expectedWarnings) - } - - @Test - fun `PendingDynamicLinkData destructure declaration works`() { - val expectedLink = Uri.parse("https://example.com") - val expectedMinAppVersion = 30 - val expectedTimestamp = 172947600L - - val mockPendingData = mock(PendingDynamicLinkData::class.java) - `when`(mockPendingData.link).thenReturn(expectedLink) - `when`(mockPendingData.minimumAppVersion).thenReturn(expectedMinAppVersion) - `when`(mockPendingData.clickTimestamp).thenReturn(expectedTimestamp) - - val (link, minAppVersion, timestamp) = mockPendingData - - assertThat(link).isEqualTo(expectedLink) - assertThat(minAppVersion).isEqualTo(expectedMinAppVersion) - assertThat(timestamp).isEqualTo(expectedTimestamp) - } + } + + assertThat(dynamicLink.uri.getQueryParameter("st")).isEqualTo(socialTitle) + assertThat(dynamicLink.uri.getQueryParameter("sd")).isEqualTo(socialDescription) + } + + @Test + fun `navigationInfoParameters type-safe builder extension works`() { + val forcedRedirect = true + + val dynamicLink = + Firebase.dynamicLinks.dynamicLink { + link = Uri.parse("https://example.com") + domainUriPrefix = "https://example.page.link" + navigationInfoParameters { forcedRedirectEnabled = true } + } + + val efr = Integer.parseInt(dynamicLink.uri.getQueryParameter("efr")!!) == 1 + assertThat(efr).isEqualTo(forcedRedirect) + } + + @Test + fun `ShortDynamicLink destructure declaration works`() { + val fakeWarning = + object : ShortDynamicLink.Warning { + override fun getMessage() = "Warning" + override fun getCode() = "warning" + } + + val expectedShortLink = Uri.parse("https://example.com") + val expectedPreviewLink = Uri.parse("https://example.com/preview") + val expectedWarnings = mutableListOf(fakeWarning) + + val mockShortDynamicLink = mock(ShortDynamicLink::class.java) + `when`(mockShortDynamicLink.shortLink).thenReturn(expectedShortLink) + `when`(mockShortDynamicLink.previewLink).thenReturn(expectedPreviewLink) + `when`(mockShortDynamicLink.warnings).thenReturn(expectedWarnings) + + val (shortLink, previewLink, warnings) = mockShortDynamicLink + + assertThat(shortLink).isEqualTo(expectedShortLink) + assertThat(previewLink).isEqualTo(expectedPreviewLink) + assertThat(warnings).isEqualTo(expectedWarnings) + } + + @Test + fun `PendingDynamicLinkData destructure declaration works`() { + val expectedLink = Uri.parse("https://example.com") + val expectedMinAppVersion = 30 + val expectedTimestamp = 172947600L + + val mockPendingData = mock(PendingDynamicLinkData::class.java) + `when`(mockPendingData.link).thenReturn(expectedLink) + `when`(mockPendingData.minimumAppVersion).thenReturn(expectedMinAppVersion) + `when`(mockPendingData.clickTimestamp).thenReturn(expectedTimestamp) + + val (link, minAppVersion, timestamp) = mockPendingData + + assertThat(link).isEqualTo(expectedLink) + assertThat(minAppVersion).isEqualTo(expectedMinAppVersion) + assertThat(timestamp).isEqualTo(expectedTimestamp) + } } diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 62e9b05b3d5..78a319a4948 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,8 +1,32 @@ # Unreleased +* [fixed] Fix an issue that stops some performance optimization being applied. + +# 24.4.1 +* [fixed] Fix `FAILED_PRECONDITION` when writing to a deleted document in a + transaction. + (#5871) + +* [fixed] Fixed [firestore] failing to raise initial snapshot from an empty + local cache result. + (#4207) + +* [fixed] Removed invalid suggestions to use `GenericTypeIndicator` from + error messages. + (#222) + +* [changed] Updated dependency of `io.grpc.*` to its latest version + (v1.50.2). + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-firestore` library. The Kotlin extensions library has no additional +updates. # 24.4.0 -* [unchanged] Updated to accommodate the release of the updated - [firestore] Kotlin extensions library. +* [feature] Added + [`Query.count()`](/docs/reference/android/com/google/firebase/firestore/Query#count()), + which fetches the number of documents in the result set without actually + downloading the documents. ## Kotlin @@ -26,7 +50,6 @@ The Kotlin extensions library transitively includes the updated ## Kotlin The Kotlin extensions library transitively includes the updated `firebase-firestore` library. - # 24.3.0 * [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). diff --git a/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties index d0583730c2c..5e34a47a03c 100644 --- a/firebase-firestore/gradle.properties +++ b/firebase-firestore/gradle.properties @@ -1,2 +1,2 @@ -version=24.3.2 -latestReleasedVersion=24.3.1 +version=24.4.2 +latestReleasedVersion=24.4.1 diff --git a/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt b/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt index 5c5867856d6..fe54a2cd2d7 100644 --- a/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt +++ b/firebase-firestore/ktx/src/main/kotlin/com/google/firebase/firestore/ktx/Firestore.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.callbackFlow /** Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. */ val Firebase.firestore: FirebaseFirestore - get() = FirebaseFirestore.getInstance() + get() = FirebaseFirestore.getInstance() /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFirestore.getInstance(app) @@ -48,7 +48,9 @@ fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFirestore. * * @param T The type of the object to create. * @return The contents of the document in an object of type T or null if the document doesn't + * ``` * exist. + * ``` */ inline fun DocumentSnapshot.toObject(): T? = toObject(T::class.java) @@ -57,14 +59,18 @@ inline fun DocumentSnapshot.toObject(): T? = toObject(T::class.java) * * @param T The type of the object to create. * @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet + * ``` * been set to their final value. - * @return The contents of the document in an object of type T or null if the document doesn't + * @return + * ``` + * The contents of the document in an object of type T or null if the document doesn't + * ``` * exist. + * ``` */ inline fun DocumentSnapshot.toObject( - serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior -): T? = - toObject(T::class.java, serverTimestampBehavior) + serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior +): T? = toObject(T::class.java, serverTimestampBehavior) /** * Returns the value at the field, converted to a POJO, or null if the field or document doesn't @@ -83,14 +89,16 @@ inline fun DocumentSnapshot.getField(field: String): T? = get(field, * @param field The path to the field. * @param T The type to convert the field value to. * @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet + * ``` * been set to their final value. - * @return The value at the given field or null. + * @return + * ``` + * The value at the given field or null. */ inline fun DocumentSnapshot.getField( - field: String, - serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior -): T? = - get(field, T::class.java, serverTimestampBehavior) + field: String, + serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior +): T? = get(field, T::class.java, serverTimestampBehavior) /** * Returns the value at the field, converted to a POJO, or null if the field or document doesn't @@ -100,7 +108,8 @@ inline fun DocumentSnapshot.getField( * @param T The type to convert the field value to. * @return The value at the given field or null. */ -inline fun DocumentSnapshot.getField(fieldPath: FieldPath): T? = get(fieldPath, T::class.java) +inline fun DocumentSnapshot.getField(fieldPath: FieldPath): T? = + get(fieldPath, T::class.java) /** * Returns the value at the field, converted to a POJO, or null if the field or document doesn't @@ -109,14 +118,16 @@ inline fun DocumentSnapshot.getField(fieldPath: FieldPath): T? = get * @param fieldPath The path to the field. * @param T The type to convert the field value to. * @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet + * ``` * been set to their final value. - * @return The value at the given field or null. + * @return + * ``` + * The value at the given field or null. */ inline fun DocumentSnapshot.getField( - fieldPath: FieldPath, - serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior -): T? = - get(fieldPath, T::class.java, serverTimestampBehavior) + fieldPath: FieldPath, + serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior +): T? = get(fieldPath, T::class.java, serverTimestampBehavior) /** * Returns the contents of the document converted to a POJO. @@ -131,36 +142,45 @@ inline fun QueryDocumentSnapshot.toObject(): T = toObject(T::c * * @param T The type of the object to create. * @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet + * ``` * been set to their final value. - * @return The contents of the document in an object of type T. + * @return + * ``` + * The contents of the document in an object of type T. */ -inline fun QueryDocumentSnapshot.toObject(serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior): T = - toObject(T::class.java, serverTimestampBehavior) +inline fun QueryDocumentSnapshot.toObject( + serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior +): T = toObject(T::class.java, serverTimestampBehavior) /** - * Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as - * a list. + * Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as a + * list. * * @param T The POJO type used to convert the documents in the list. */ inline fun QuerySnapshot.toObjects(): List = toObjects(T::class.java) /** - * Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as - * a list. + * Returns the contents of the documents in the QuerySnapshot, converted to the provided class, as a + * list. * * @param T The POJO type used to convert the documents in the list. * @param serverTimestampBehavior Configures the behavior for server timestamps that have not yet + * ``` * been set to their final value. + * ``` */ -inline fun QuerySnapshot.toObjects(serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior): List = - toObjects(T::class.java, serverTimestampBehavior) +inline fun QuerySnapshot.toObjects( + serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior +): List = toObjects(T::class.java, serverTimestampBehavior) /** Returns a [FirebaseFirestoreSettings] instance initialized using the [init] function. */ -fun firestoreSettings(init: FirebaseFirestoreSettings.Builder.() -> Unit): FirebaseFirestoreSettings { - val builder = FirebaseFirestoreSettings.Builder() - builder.init() - return builder.build() +fun firestoreSettings( + init: FirebaseFirestoreSettings.Builder.() -> Unit +): FirebaseFirestoreSettings { + val builder = FirebaseFirestoreSettings.Builder() + builder.init() + return builder.build() } internal const val LIBRARY_NAME: String = "fire-fst-ktx" @@ -168,12 +188,13 @@ internal const val LIBRARY_NAME: String = "fire-fst-ktx" /** @suppress */ @Keep class FirebaseFirestoreKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } /** - * Starts listening to the document referenced by this `DocumentReference` with the given options and emits its values via a [Flow]. + * Starts listening to the document referenced by this `DocumentReference` with the given options + * and emits its values via a [Flow]. * * - When the returned flow starts being collected, an [EventListener] will be attached. * - When the flow completes, the listener will be removed. @@ -181,18 +202,19 @@ class FirebaseFirestoreKtxRegistrar : ComponentRegistrar { * @param metadataChanges controls metadata-only changes. Default: [MetadataChanges.EXCLUDE] */ fun DocumentReference.snapshots( - metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE + metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE ): Flow { - return callbackFlow { - val registration = addSnapshotListener(BACKGROUND_EXECUTOR, metadataChanges) { snapshot, exception -> - if (exception != null) { - cancel(message = "Error getting DocumentReference snapshot", cause = exception) - } else if (snapshot != null) { - trySendBlocking(snapshot) - } + return callbackFlow { + val registration = + addSnapshotListener(BACKGROUND_EXECUTOR, metadataChanges) { snapshot, exception -> + if (exception != null) { + cancel(message = "Error getting DocumentReference snapshot", cause = exception) + } else if (snapshot != null) { + trySendBlocking(snapshot) } - awaitClose { registration.remove() } - } + } + awaitClose { registration.remove() } + } } /** @@ -204,16 +226,17 @@ fun DocumentReference.snapshots( * @param metadataChanges controls metadata-only changes. Default: [MetadataChanges.EXCLUDE] */ fun Query.snapshots( - metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE + metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE ): Flow { - return callbackFlow { - val registration = addSnapshotListener(BACKGROUND_EXECUTOR, metadataChanges) { snapshot, exception -> - if (exception != null) { - cancel(message = "Error getting Query snapshot", cause = exception) - } else if (snapshot != null) { - trySendBlocking(snapshot) - } + return callbackFlow { + val registration = + addSnapshotListener(BACKGROUND_EXECUTOR, metadataChanges) { snapshot, exception -> + if (exception != null) { + cancel(message = "Error getting Query snapshot", cause = exception) + } else if (snapshot != null) { + trySendBlocking(snapshot) } - awaitClose { registration.remove() } - } + } + awaitClose { registration.remove() } + } } 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 index 37359d5a080..e11c5af1503 100644 --- 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 @@ -105,7 +105,8 @@ public static QuerySnapshot querySnapshot( isFromCache, mutatedKeys, /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ false); + /* excludesMetadataChanges= */ false, + /* hasCachedResults= */ false); return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE); } } diff --git a/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt b/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt index 061ab7b3fbd..4c8a32250c4 100644 --- a/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt +++ b/firebase-firestore/ktx/src/test/kotlin/com/google/firebase/firestore/ktx/FirestoreTests.kt @@ -42,140 +42,147 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class FirestoreTests : BaseTestCase() { - @Test - fun `firestore should delegate to FirebaseFirestore#getInstance()`() { - assertThat(Firebase.firestore).isSameInstanceAs(FirebaseFirestore.getInstance()) + @Test + fun `firestore should delegate to FirebaseFirestore#getInstance()`() { + assertThat(Firebase.firestore).isSameInstanceAs(FirebaseFirestore.getInstance()) + } + + @Test + fun `FirebaseApp#firestore should delegate to FirebaseFirestore#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.firestore(app)).isSameInstanceAs(FirebaseFirestore.getInstance(app)) + } + + @Test + fun `FirebaseFirestoreSettings builder works`() { + val host = "http://10.0.2.2:8080" + val isSslEnabled = false + val isPersistenceEnabled = false + + val settings = firestoreSettings { + this.host = host + this.isSslEnabled = isSslEnabled + this.isPersistenceEnabled = isPersistenceEnabled } - @Test - fun `FirebaseApp#firestore should delegate to FirebaseFirestore#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.firestore(app)).isSameInstanceAs(FirebaseFirestore.getInstance(app)) - } - - @Test - fun `FirebaseFirestoreSettings builder works`() { - val host = "http://10.0.2.2:8080" - val isSslEnabled = false - val isPersistenceEnabled = false - - val settings = firestoreSettings { - this.host = host - this.isSslEnabled = isSslEnabled - this.isPersistenceEnabled = isPersistenceEnabled - } - - assertThat(host).isEqualTo(settings.host) - assertThat(isSslEnabled).isEqualTo(settings.isSslEnabled) - assertThat(isPersistenceEnabled).isEqualTo(settings.isPersistenceEnabled) - } + assertThat(host).isEqualTo(settings.host) + assertThat(isSslEnabled).isEqualTo(settings.isSslEnabled) + assertThat(isPersistenceEnabled).isEqualTo(settings.isPersistenceEnabled) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } data class Room(var a: Int = 0, var b: Int = 0) @RunWith(RobolectricTestRunner::class) class DocumentSnapshotTests { - @Before - fun setup() { - Mockito.`when`(TestUtil.firestore().firestoreSettings).thenReturn(FirebaseFirestoreSettings.Builder().build()) - } - - @After - fun cleanup() { - Mockito.reset(TestUtil.firestore()) - } - - @Test - fun `reified getField delegates to get()`() { - val ds = TestUtil.documentSnapshot("rooms/foo", mapOf("a" to 1, "b" to 2), false) - - assertThat(ds.getField("a")).isEqualTo(ds.get("a", Int::class.java)) - assertThat(ds.getField(FieldPath.of("a"))) - .isEqualTo(ds.get(FieldPath.of("a"), Int::class.java)) - - assertThat(ds.getField("a", DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) - .isEqualTo(ds.get("a", Int::class.java, DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) - assertThat(ds.getField(FieldPath.of("a"), DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) - .isEqualTo(ds.get(FieldPath.of("a"), Int::class.java)) - } - - @Test - fun `reified toObject delegates to toObject(Class)`() { - val ds = TestUtil.documentSnapshot("rooms/foo", mapOf("a" to 1, "b" to 2), false) - - var room = ds.toObject() - assertThat(room).isEqualTo(Room(1, 2)) - assertThat(room).isEqualTo(ds.toObject(Room::class.java)) - - room = ds.toObject(DocumentSnapshot.ServerTimestampBehavior.ESTIMATE) - assertThat(room).isEqualTo(Room(1, 2)) - assertThat(room) - .isEqualTo(ds.toObject(Room::class.java, DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) - } + @Before + fun setup() { + Mockito.`when`(TestUtil.firestore().firestoreSettings) + .thenReturn(FirebaseFirestoreSettings.Builder().build()) + } + + @After + fun cleanup() { + Mockito.reset(TestUtil.firestore()) + } + + @Test + fun `reified getField delegates to get()`() { + val ds = TestUtil.documentSnapshot("rooms/foo", mapOf("a" to 1, "b" to 2), false) + + assertThat(ds.getField("a")).isEqualTo(ds.get("a", Int::class.java)) + assertThat(ds.getField(FieldPath.of("a"))) + .isEqualTo(ds.get(FieldPath.of("a"), Int::class.java)) + + assertThat(ds.getField("a", DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) + .isEqualTo(ds.get("a", Int::class.java, DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) + assertThat( + ds.getField(FieldPath.of("a"), DocumentSnapshot.ServerTimestampBehavior.ESTIMATE) + ) + .isEqualTo(ds.get(FieldPath.of("a"), Int::class.java)) + } + + @Test + fun `reified toObject delegates to toObject(Class)`() { + val ds = TestUtil.documentSnapshot("rooms/foo", mapOf("a" to 1, "b" to 2), false) + + var room = ds.toObject() + assertThat(room).isEqualTo(Room(1, 2)) + assertThat(room).isEqualTo(ds.toObject(Room::class.java)) + + room = ds.toObject(DocumentSnapshot.ServerTimestampBehavior.ESTIMATE) + assertThat(room).isEqualTo(Room(1, 2)) + assertThat(room) + .isEqualTo(ds.toObject(Room::class.java, DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) + } } @RunWith(RobolectricTestRunner::class) class QuerySnapshotTests { - @Before - fun setup() { - Mockito.`when`(TestUtil.firestore().firestoreSettings).thenReturn(FirebaseFirestoreSettings.Builder().build()) - } - - @After - fun cleanup() { - Mockito.reset(TestUtil.firestore()) - } - - @Test - fun `reified toObjects delegates to toObjects(Class)`() { - val qs = TestUtil.querySnapshot( - "rooms", - mapOf(), - mapOf("id" to ObjectValue.fromMap(mapOf("a" to wrap(1), "b" to wrap(2)))), - false, - false) - - assertThat(qs.toObjects()).containsExactly(Room(1, 2)) - assertThat(qs.toObjects(DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)).containsExactly(Room(1, 2)) - } + @Before + fun setup() { + Mockito.`when`(TestUtil.firestore().firestoreSettings) + .thenReturn(FirebaseFirestoreSettings.Builder().build()) + } + + @After + fun cleanup() { + Mockito.reset(TestUtil.firestore()) + } + + @Test + fun `reified toObjects delegates to toObjects(Class)`() { + val qs = + TestUtil.querySnapshot( + "rooms", + mapOf(), + mapOf("id" to ObjectValue.fromMap(mapOf("a" to wrap(1), "b" to wrap(2)))), + false, + false + ) + + assertThat(qs.toObjects()).containsExactly(Room(1, 2)) + assertThat(qs.toObjects(DocumentSnapshot.ServerTimestampBehavior.ESTIMATE)) + .containsExactly(Room(1, 2)) + } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java index 5636cecffdd..396847e2174 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CountTest.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore; +import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore; @@ -25,10 +26,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.testutil.IntegrationTestUtil; +import java.util.Collections; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -253,4 +258,23 @@ public void testFailWithoutNetwork() { AggregateQuerySnapshot snapshot = waitFor(collection.count().get(AggregateSource.SERVER)); assertEquals(3L, snapshot.getCount()); } + + @Test + public void testFailWithGoodMessageIfMissingIndex() { + assumeFalse( + "Skip this test when running against the Firestore emulator because the Firestore emulator " + + "does not use indexes and never fails with a 'missing index' error", + BuildConfig.USE_EMULATOR_FOR_TESTS); + + CollectionReference collection = testCollectionWithDocs(Collections.emptyMap()); + Query compositeIndexQuery = collection.whereEqualTo("field1", 42).whereLessThan("field2", 99); + AggregateQuery compositeIndexCountQuery = compositeIndexQuery.count(); + Task task = compositeIndexCountQuery.get(AggregateSource.SERVER); + + Throwable throwable = assertThrows(Throwable.class, () -> waitFor(task)); + + Throwable cause = throwable.getCause(); + assertThat(cause).hasMessageThat().ignoringCase().contains("index"); + assertThat(cause).hasMessageThat().contains("https://console.firebase.google.com"); + } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java index f547408410e..c2eef0989c2 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java @@ -550,6 +550,50 @@ public void testQueriesFireFromCacheWhenOffline() { listener.remove(); } + @Test + public void testQueriesCanRaiseInitialSnapshotFromCachedEmptyResults() { + CollectionReference collectionReference = testCollection(); + + // Populate the cache with empty query result. + QuerySnapshot querySnapshotA = waitFor(collectionReference.get()); + assertFalse(querySnapshotA.getMetadata().isFromCache()); + assertEquals(asList(), querySnapshotToValues(querySnapshotA)); + + // Add a snapshot listener whose first event should be raised from cache. + EventAccumulator accum = new EventAccumulator<>(); + ListenerRegistration listenerRegistration = + collectionReference.addSnapshotListener(accum.listener()); + QuerySnapshot querySnapshotB = accum.await(); + assertTrue(querySnapshotB.getMetadata().isFromCache()); + assertEquals(asList(), querySnapshotToValues(querySnapshotB)); + + listenerRegistration.remove(); + } + + @Test + public void testQueriesCanRaiseInitialSnapshotFromEmptyDueToDeleteCachedResults() { + Map> testDocs = map("a", map("foo", 1L)); + CollectionReference collectionReference = testCollectionWithDocs(testDocs); + // Populate the cache with single document. + QuerySnapshot querySnapshotA = waitFor(collectionReference.get()); + assertFalse(querySnapshotA.getMetadata().isFromCache()); + assertEquals(asList(testDocs.get("a")), querySnapshotToValues(querySnapshotA)); + + // delete the document, make cached result empty. + DocumentReference docRef = collectionReference.document("a"); + waitFor(docRef.delete()); + + // Add a snapshot listener whose first event should be raised from cache. + EventAccumulator accum = new EventAccumulator<>(); + ListenerRegistration listenerRegistration = + collectionReference.addSnapshotListener(accum.listener()); + QuerySnapshot querySnapshotB = accum.await(); + assertTrue(querySnapshotB.getMetadata().isFromCache()); + assertEquals(asList(), querySnapshotToValues(querySnapshotB)); + + listenerRegistration.remove(); + } + @Test public void testQueriesCanUseNotEqualFilters() { // These documents are ordered by value in "zip" since the notEquals filter is an inequality, diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java index 4045cdc0e06..f83289d3d5a 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java @@ -69,12 +69,21 @@ void runStage(Transaction transaction, DocumentReference docRef) private static TransactionStage get = Transaction::get; + private enum FromDocumentType { + // The operation will be performed on a document that exists. + EXISTING, + // The operation will be performed on a document that has never existed. + NON_EXISTENT, + // The operation will be performed on a document that existed, but was deleted. + DELETED, + } + /** * Used for testing that all possible combinations of executing transactions result in the desired * document value or error. * - *

`run()`, `withExistingDoc()`, and `withNonexistentDoc()` don't actually do anything except - * assign variables into the TransactionTester. + *

`run()`, `withExistingDoc()`, `withNonexistentDoc()` and `withDeletedDoc()` don't actually + * do anything except assign variables into the TransactionTester. * *

`expectDoc()`, `expectNoDoc()`, and `expectError()` will trigger the transaction to run and * assert that the end result matches the input. @@ -82,7 +91,7 @@ void runStage(Transaction transaction, DocumentReference docRef) private static class TransactionTester { private FirebaseFirestore db; private DocumentReference docRef; - private boolean fromExistingDoc = false; + private FromDocumentType fromDocumentType = FromDocumentType.NON_EXISTENT; private List stages = new ArrayList<>(); TransactionTester(FirebaseFirestore inputDb) { @@ -91,13 +100,19 @@ private static class TransactionTester { @CanIgnoreReturnValue public TransactionTester withExistingDoc() { - fromExistingDoc = true; + fromDocumentType = FromDocumentType.EXISTING; return this; } @CanIgnoreReturnValue public TransactionTester withNonexistentDoc() { - fromExistingDoc = false; + fromDocumentType = FromDocumentType.NON_EXISTENT; + return this; + } + + @CanIgnoreReturnValue + public TransactionTester withDeletedDoc() { + fromDocumentType = FromDocumentType.DELETED; return this; } @@ -160,8 +175,20 @@ private void expectError(Code expected) { private void prepareDoc() { docRef = db.collection("tester-docref").document(); - if (fromExistingDoc) { - waitFor(docRef.set(map("foo", "bar0"))); + + switch (fromDocumentType) { + case EXISTING: + waitFor(docRef.set(map("foo", "bar0"))); + break; + case NON_EXISTENT: + // Nothing to do; document does not exist. + break; + case DELETED: + waitFor(docRef.set(map("foo", "bar0"))); + waitFor(docRef.delete()); + break; + default: + throw new RuntimeException("invalid fromDocumentType: " + fromDocumentType); } } @@ -241,6 +268,29 @@ public void testRunsTransactionsAfterGettingNonexistentDoc() { tt.withNonexistentDoc().run(get, set1, set2).expectDoc(map("foo", "bar2")); } + // This test is identical to the test above, except that withNonexistentDoc() + // is replaced by withDeletedDoc(), to guard against regression of + // https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions + // would incorrectly fail with FAILED_PRECONDITION when operations were + // performed on a deleted document (rather than a non-existent document). + @Test + public void testRunsTransactionsAfterGettingDeletedDoc() { + FirebaseFirestore firestore = testFirestore(); + TransactionTester tt = new TransactionTester(firestore); + + tt.withDeletedDoc().run(get, delete1, delete1).expectNoDoc(); + tt.withDeletedDoc().run(get, delete1, update2).expectError(Code.INVALID_ARGUMENT); + tt.withDeletedDoc().run(get, delete1, set2).expectDoc(map("foo", "bar2")); + + tt.withDeletedDoc().run(get, update1, delete1).expectError(Code.INVALID_ARGUMENT); + tt.withDeletedDoc().run(get, update1, update2).expectError(Code.INVALID_ARGUMENT); + tt.withDeletedDoc().run(get, update1, set2).expectError(Code.INVALID_ARGUMENT); + + tt.withDeletedDoc().run(get, set1, delete1).expectNoDoc(); + tt.withDeletedDoc().run(get, set1, update2).expectDoc(map("foo", "bar2")); + tt.withDeletedDoc().run(get, set1, set2).expectDoc(map("foo", "bar2")); + } + @Test public void testRunsTransactionsOnExistingDoc() { FirebaseFirestore firestore = testFirestore(); @@ -277,6 +327,24 @@ public void testRunsTransactionsOnNonexistentDoc() { tt.withNonexistentDoc().run(set1, set2).expectDoc(map("foo", "bar2")); } + @Test + public void testRunsTransactionsOnDeletedDoc() { + FirebaseFirestore firestore = testFirestore(); + TransactionTester tt = new TransactionTester(firestore); + + tt.withDeletedDoc().run(delete1, delete1).expectNoDoc(); + tt.withDeletedDoc().run(delete1, update2).expectError(Code.INVALID_ARGUMENT); + tt.withDeletedDoc().run(delete1, set2).expectDoc(map("foo", "bar2")); + + tt.withDeletedDoc().run(update1, delete1).expectError(Code.NOT_FOUND); + tt.withDeletedDoc().run(update1, update2).expectError(Code.NOT_FOUND); + tt.withDeletedDoc().run(update1, set2).expectError(Code.NOT_FOUND); + + tt.withDeletedDoc().run(set1, delete1).expectNoDoc(); + tt.withDeletedDoc().run(set1, update2).expectDoc(map("foo", "bar2")); + tt.withDeletedDoc().run(set1, set2).expectDoc(map("foo", "bar2")); + } + @Test public void testSetDocumentWithMerge() { FirebaseFirestore firestore = testFirestore(); @@ -637,6 +705,29 @@ public void testDoesNotRetryOnPermanentError() { assertEquals(1, count.get()); } + @Test + public void testRetryOnAlreadyExistsError() { + final FirebaseFirestore firestore = testFirestore(); + DocumentReference doc = firestore.collection("foo").document(); + AtomicInteger transactionCallbackCount = new AtomicInteger(0); + waitFor( + firestore.runTransaction( + transaction -> { + int currentCount = transactionCallbackCount.incrementAndGet(); + transaction.get(doc); + // Do a write outside of the transaction. + if (currentCount == 1) waitFor(doc.set(map("foo1", "bar1"))); + // Now try to set the doc within the transaction. This should fail once + // with ALREADY_EXISTS error. + transaction.set(doc, map("foo2", "bar2")); + return null; + })); + DocumentSnapshot snapshot = waitFor(doc.get()); + assertEquals(2, transactionCallbackCount.get()); + assertTrue(snapshot.exists()); + assertEquals(map("foo2", "bar2"), snapshot.getData()); + } + @Test public void testMakesDefaultMaxAttempts() { FirebaseFirestore firestore = testFirestore(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/Timestamp.java b/firebase-firestore/src/main/java/com/google/firebase/Timestamp.java index f0b16987125..bd045ae5ad4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/Timestamp.java +++ b/firebase-firestore/src/main/java/com/google/firebase/Timestamp.java @@ -30,9 +30,7 @@ * 9999-12-31T23:59:59.999999999Z. By restricting to that range, we ensure that we can convert to * and from RFC 3339 date strings. * - * @see The - * reference timestamp definition + * @see TimestampThe ref timestamp definition */ public final class Timestamp implements Comparable, Parcelable { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Filter.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Filter.java index 0df73bce122..534b093dc6a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Filter.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Filter.java @@ -17,13 +17,17 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; -import com.google.firebase.firestore.core.CompositeFilter; import com.google.firebase.firestore.core.FieldFilter.Operator; import java.util.Arrays; import java.util.List; +// TODO(orquery): Remove the `hide` and scope annotations. /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) +/** + * A {@code Filter} represents a restriction on one or more field values and can be used to refine + * the results of a {@code Query}. + */ public class Filter { static class UnaryFilter extends Filter { private final FieldPath field; @@ -70,112 +74,274 @@ public com.google.firebase.firestore.core.CompositeFilter.Operator getOperator() } } + /** + * Creates a new filter for checking that the given field is equal to the given value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter equalTo(@NonNull String field, @Nullable Object value) { return equalTo(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is equal to the given value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter equalTo(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.EQUAL, value); } + /** + * Creates a new filter for checking that the given field is not equal to the given value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter notEqualTo(@NonNull String field, @Nullable Object value) { return notEqualTo(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is not equal to the given value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter notEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.NOT_EQUAL, value); } + /** + * Creates a new filter for checking that the given field is greater than the given value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter greaterThan(@NonNull String field, @Nullable Object value) { return greaterThan(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is greater than the given value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter greaterThan(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.GREATER_THAN, value); } + /** + * Creates a new filter for checking that the given field is greater than or equal to the given + * value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter greaterThanOrEqualTo(@NonNull String field, @Nullable Object value) { return greaterThanOrEqualTo(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is greater than or equal to the given + * value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter greaterThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.GREATER_THAN_OR_EQUAL, value); } + /** + * Creates a new filter for checking that the given field is less than the given value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter lessThan(@NonNull String field, @Nullable Object value) { return lessThan(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is less than the given value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter lessThan(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.LESS_THAN, value); } + /** + * Creates a new filter for checking that the given field is less than or equal to the given + * value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter lessThanOrEqualTo(@NonNull String field, @Nullable Object value) { return lessThanOrEqualTo(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given field is less than or equal to the given + * value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter lessThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.LESS_THAN_OR_EQUAL, value); } + /** + * Creates a new filter for checking that the given array field contains the given value. + * + * @param field The field used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter arrayContains(@NonNull String field, @Nullable Object value) { return arrayContains(FieldPath.fromDotSeparatedPath(field), value); } + /** + * Creates a new filter for checking that the given array field contains the given value. + * + * @param fieldPath The field path used for the filter. + * @param value The value used for the filter. + * @return The newly created filter. + */ @NonNull public static Filter arrayContains(@NonNull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS, value); } + /** + * Creates a new filter for checking that the given array field contains any of the given values. + * + * @param field The field used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter arrayContainsAny(@NonNull String field, @Nullable Object value) { - return arrayContainsAny(FieldPath.fromDotSeparatedPath(field), value); + public static Filter arrayContainsAny( + @NonNull String field, @NonNull List values) { + return arrayContainsAny(FieldPath.fromDotSeparatedPath(field), values); } + /** + * Creates a new filter for checking that the given array field contains any of the given values. + * + * @param fieldPath The field path used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter arrayContainsAny(@NonNull FieldPath fieldPath, @Nullable Object value) { - return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS_ANY, value); + public static Filter arrayContainsAny( + @NonNull FieldPath fieldPath, @NonNull List values) { + return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS_ANY, values); } + /** + * Creates a new filter for checking that the given field equals any of the given values. + * + * @param field The field used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter inArray(@NonNull String field, @Nullable Object value) { - return inArray(FieldPath.fromDotSeparatedPath(field), value); + public static Filter inArray(@NonNull String field, @NonNull List values) { + return inArray(FieldPath.fromDotSeparatedPath(field), values); } + /** + * Creates a new filter for checking that the given field equals any of the given values. + * + * @param fieldPath The field path used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter inArray(@NonNull FieldPath fieldPath, @Nullable Object value) { - return new UnaryFilter(fieldPath, Operator.IN, value); + public static Filter inArray( + @NonNull FieldPath fieldPath, @NonNull List values) { + return new UnaryFilter(fieldPath, Operator.IN, values); } + /** + * Creates a new filter for checking that the given field does not equal any of the given values. + * + * @param field The field path used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter notInArray(@NonNull String field, @Nullable Object value) { - return notInArray(FieldPath.fromDotSeparatedPath(field), value); + public static Filter notInArray(@NonNull String field, @NonNull List values) { + return notInArray(FieldPath.fromDotSeparatedPath(field), values); } + /** + * Creates a new filter for checking that the given field does not equal any of the given values. + * + * @param fieldPath The field path used for the filter. + * @param values The list of values used for the filter. + * @return The newly created filter. + */ @NonNull - public static Filter notInArray(@NonNull FieldPath fieldPath, @Nullable Object value) { - return new UnaryFilter(fieldPath, Operator.NOT_IN, value); + public static Filter notInArray( + @NonNull FieldPath fieldPath, @NonNull List values) { + return new UnaryFilter(fieldPath, Operator.NOT_IN, values); } + /** + * Creates a new filter that is a disjunction of the given filters. A disjunction filter includes + * a document if it satisfies any of the given filters. + * + * @param filters The list of filters to perform a disjunction for. + * @return The newly created filter. + */ @NonNull public static Filter or(Filter... filters) { return new CompositeFilter( Arrays.asList(filters), com.google.firebase.firestore.core.CompositeFilter.Operator.OR); } + /** + * Creates a new filter that is a conjunction of the given filters. A conjunction filter includes + * a document if it satisfies all of the given filters. + * + * @param filters The list of filters to perform a conjunction for. + * @return The newly created filter. + */ @NonNull public static Filter and(Filter... filters) { return new CompositeFilter( diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 7740a678f6c..62b4214b91f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import static com.google.firebase.firestore.util.Preconditions.checkNotNull; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import androidx.annotation.Keep; @@ -737,6 +738,8 @@ public LoadBundleTask loadBundle(@NonNull ByteBuffer bundleData) { * documents) and loaded to local cache using {@link #loadBundle(byte[])}. Once in local cache, * you can use this method to extract a query by name. */ + // TODO(b/261013682): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public @NonNull Task getNamedQuery(@NonNull String name) { ensureClientConfigured(); return client diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index 8db1e9ff8db..a4519aacfb8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -388,6 +388,23 @@ public Query whereNotIn(@NonNull FieldPath fieldPath, @NonNull List eventManager.removeQueryListener(listener)); } + // TODO(b/261013682): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public Task getDocumentFromLocalCache(DocumentKey docKey) { this.verifyNotTerminated(); return asyncQueue @@ -237,6 +240,8 @@ public Task transaction( () -> syncEngine.transaction(asyncQueue, options, updateFunction)); } + // TODO(b/261013682): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") public Task runCountQuery(Query query) { this.verifyNotTerminated(); final TaskCompletionSource result = new TaskCompletionSource<>(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java index b5201cfda43..a041da9191b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/QueryListener.java @@ -87,7 +87,8 @@ public boolean onViewSnapshot(ViewSnapshot newSnapshot) { newSnapshot.isFromCache(), newSnapshot.getMutatedKeys(), newSnapshot.didSyncStateChange(), - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + newSnapshot.hasCachedResults()); } if (!raisedInitialEvent) { @@ -139,8 +140,11 @@ private boolean shouldRaiseInitialEvent(ViewSnapshot snapshot, OnlineState onlin return false; } - // Raise data from cache if we have any documents or we are offline - return !snapshot.getDocuments().isEmpty() || onlineState.equals(OnlineState.OFFLINE); + // Raise data from cache if we have any documents, have cached results before, + // or we are offline. + return (!snapshot.getDocuments().isEmpty() + || snapshot.hasCachedResults() + || onlineState.equals(OnlineState.OFFLINE)); } private boolean shouldRaiseEvent(ViewSnapshot snapshot) { @@ -171,7 +175,8 @@ private void raiseInitialEvent(ViewSnapshot snapshot) { snapshot.getDocuments(), snapshot.getMutatedKeys(), snapshot.isFromCache(), - snapshot.excludesMetadataChanges()); + snapshot.excludesMetadataChanges(), + snapshot.hasCachedResults()); raisedInitialEvent = true; listener.onEvent(snapshot, null); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 7513e2b9f27..c7da9e27e18 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -54,6 +54,7 @@ import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; +import com.google.protobuf.ByteString; import io.grpc.Status; import java.io.IOException; import java.util.ArrayList; @@ -204,13 +205,16 @@ public int listen(Query query) { TargetData targetData = localStore.allocateTarget(query.toTarget()); remoteStore.listen(targetData); - ViewSnapshot viewSnapshot = initializeViewAndComputeSnapshot(query, targetData.getTargetId()); + ViewSnapshot viewSnapshot = + initializeViewAndComputeSnapshot( + query, targetData.getTargetId(), targetData.getResumeToken()); syncEngineListener.onViewSnapshots(Collections.singletonList(viewSnapshot)); return targetData.getTargetId(); } - private ViewSnapshot initializeViewAndComputeSnapshot(Query query, int targetId) { + private ViewSnapshot initializeViewAndComputeSnapshot( + Query query, int targetId, ByteString resumeToken) { QueryResult queryResult = localStore.executeQuery(query, /* usePreviousResults= */ true); SyncState currentTargetSyncState = SyncState.NONE; @@ -221,10 +225,10 @@ private ViewSnapshot initializeViewAndComputeSnapshot(Query query, int targetId) if (this.queriesByTarget.get(targetId) != null) { Query mirrorQuery = this.queriesByTarget.get(targetId).get(0); currentTargetSyncState = this.queryViewsByQuery.get(mirrorQuery).getView().getSyncState(); - synthesizedCurrentChange = - TargetChange.createSynthesizedTargetChangeForCurrentChange( - currentTargetSyncState == SyncState.SYNCED); } + synthesizedCurrentChange = + TargetChange.createSynthesizedTargetChangeForCurrentChange( + currentTargetSyncState == SyncState.SYNCED, resumeToken); // TODO(wuandy): Investigate if we can extract the logic of view change computation and // update tracked limbo in one place, and have both emitNewSnapsAndNotifyLocalStore diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Transaction.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Transaction.java index 4a2cdbc3321..42c62125dd1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Transaction.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Transaction.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; +import android.annotation.SuppressLint; import androidx.annotation.Nullable; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; @@ -162,6 +163,9 @@ private static Executor createDefaultExecutor() { int maxPoolSize = corePoolSize; int keepAliveSeconds = 1; LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveSeconds, TimeUnit.SECONDS, queue); @@ -199,7 +203,11 @@ private void recordVersion(MutableDocument doc) throws FirebaseFirestoreExceptio private Precondition precondition(DocumentKey key) { @Nullable SnapshotVersion version = readVersions.get(key); if (!writtenDocs.contains(key) && version != null) { - return Precondition.updateTime(version); + if (version.equals(SnapshotVersion.NONE)) { + return Precondition.exists(false); + } else { + return Precondition.updateTime(version); + } } else { return Precondition.NONE; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java index 70a249cc804..28db0e6902e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java @@ -95,10 +95,12 @@ private void handleTransactionError(Task task) { private static boolean isRetryableTransactionError(Exception e) { if (e instanceof FirebaseFirestoreException) { - // In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and - // non-matching document versions with ABORTED. These errors should be retried. + // In transactions, the backend will fail outdated reads with FAILED_PRECONDITION, + // non-matching document versions with ABORTED, and attempts to create already exists with + // ALREADY_EXISTS. These errors should be retried. FirebaseFirestoreException.Code code = ((FirebaseFirestoreException) e).getCode(); return code == FirebaseFirestoreException.Code.ABORTED + || code == FirebaseFirestoreException.Code.ALREADY_EXISTS || code == FirebaseFirestoreException.Code.FAILED_PRECONDITION || !Datastore.isPermanentError(((FirebaseFirestoreException) e).getCode()); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java index 4248998f9b2..7e1cfcf110a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/View.java @@ -300,8 +300,11 @@ public ViewChange applyChanges(DocumentChanges docChanges, TargetChange targetCh boolean syncStatedChanged = newSyncState != syncState; syncState = newSyncState; ViewSnapshot snapshot = null; + if (viewChanges.size() != 0 || syncStatedChanged) { boolean fromCache = newSyncState == SyncState.LOCAL; + boolean hasCachedResults = + targetChange == null ? false : !targetChange.getResumeToken().isEmpty(); snapshot = new ViewSnapshot( query, @@ -311,7 +314,8 @@ public ViewChange applyChanges(DocumentChanges docChanges, TargetChange targetCh fromCache, docChanges.mutatedKeys, syncStatedChanged, - /* excludesMetadataChanges= */ false); + /* excludesMetadataChanges= */ false, + hasCachedResults); } return new ViewChange(snapshot, limboDocumentChanges); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java index 1e05a3d8e98..2741815c1d4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ViewSnapshot.java @@ -39,6 +39,7 @@ public enum SyncState { private final ImmutableSortedSet mutatedKeys; private final boolean didSyncStateChange; private boolean excludesMetadataChanges; + private boolean hasCachedResults; public ViewSnapshot( Query query, @@ -48,7 +49,8 @@ public ViewSnapshot( boolean isFromCache, ImmutableSortedSet mutatedKeys, boolean didSyncStateChange, - boolean excludesMetadataChanges) { + boolean excludesMetadataChanges, + boolean hasCachedResults) { this.query = query; this.documents = documents; this.oldDocuments = oldDocuments; @@ -57,6 +59,7 @@ public ViewSnapshot( this.mutatedKeys = mutatedKeys; this.didSyncStateChange = didSyncStateChange; this.excludesMetadataChanges = excludesMetadataChanges; + this.hasCachedResults = hasCachedResults; } /** Returns a view snapshot as if all documents in the snapshot were added. */ @@ -65,7 +68,8 @@ public static ViewSnapshot fromInitialDocuments( DocumentSet documents, ImmutableSortedSet mutatedKeys, boolean fromCache, - boolean excludesMetadataChanges) { + boolean excludesMetadataChanges, + boolean hasCachedResults) { List viewChanges = new ArrayList<>(); for (Document doc : documents) { viewChanges.add(DocumentViewChange.create(DocumentViewChange.Type.ADDED, doc)); @@ -78,7 +82,8 @@ public static ViewSnapshot fromInitialDocuments( fromCache, mutatedKeys, /* didSyncStateChange= */ true, - excludesMetadataChanges); + excludesMetadataChanges, + hasCachedResults); } public Query getQuery() { @@ -117,6 +122,10 @@ public boolean excludesMetadataChanges() { return excludesMetadataChanges; } + public boolean hasCachedResults() { + return hasCachedResults; + } + @Override public final boolean equals(Object o) { if (this == o) { @@ -149,6 +158,9 @@ public final boolean equals(Object o) { if (!oldDocuments.equals(that.oldDocuments)) { return false; } + if (hasCachedResults != that.hasCachedResults) { + return false; + } return changes.equals(that.changes); } @@ -162,6 +174,7 @@ public int hashCode() { result = 31 * result + (isFromCache ? 1 : 0); result = 31 * result + (didSyncStateChange ? 1 : 0); result = 31 * result + (excludesMetadataChanges ? 1 : 0); + result = 31 * result + (hasCachedResults ? 1 : 0); return result; } @@ -183,6 +196,8 @@ public String toString() { + didSyncStateChange + ", excludesMetadataChanges=" + excludesMetadataChanges + + ", hasCachedResults=" + + hasCachedResults + ")"; } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java index 9642f1bbdc6..c2dfc626fb7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalDocumentsView.java @@ -160,6 +160,7 @@ private Map computeViews( Map mutatedFields = new HashMap<>(); for (MutableDocument doc : docs.values()) { Overlay overlay = overlays.get(doc.getKey()); + // Recalculate an overlay if the document's existence state is changed due to a remote // event *and* the overlay is a PatchMutation. This is because document existence state // can change if some patch mutation's preconditions are met. @@ -174,6 +175,9 @@ private Map computeViews( overlay .getMutation() .applyToLocalView(doc, overlay.getMutation().getFieldMask(), Timestamp.now()); + } else { // overlay == null + // Using EMPTY to indicate there is no overlay for the document. + mutatedFields.put(doc.getKey(), FieldMask.EMPTY); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/OverlayedDocument.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/OverlayedDocument.java index 50a82e6c94f..e2b791bce06 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/OverlayedDocument.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/OverlayedDocument.java @@ -21,7 +21,7 @@ /** Represents a local view (overlay) of a document, and the fields that are locally mutated. */ public class OverlayedDocument { private Document overlayedDocument; - private FieldMask mutatedFields; + @Nullable private FieldMask mutatedFields; OverlayedDocument(Document overlayedDocument, FieldMask mutatedFields) { this.overlayedDocument = overlayedDocument; @@ -33,8 +33,11 @@ public Document getDocument() { } /** - * The fields that are locally mutated by patch mutations. If the overlayed document is from set - * or delete mutations, this returns null. + * The fields that are locally mutated by patch mutations. + * + *

If the overlayed document is from set or delete mutations, returns null. + * + *

If there is no overlay (mutation) for the document, returns FieldMask.EMPTY. */ public @Nullable FieldMask getMutatedFields() { return mutatedFields; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java index f5e5496a4c6..8f0d79f4ecd 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java @@ -23,6 +23,8 @@ import static java.lang.Math.max; import android.text.TextUtils; +import android.util.Pair; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedMap; @@ -476,13 +478,19 @@ public List getDocumentsMatchingTarget(Target target) { List subQueries = new ArrayList<>(); List bindings = new ArrayList<>(); + List> indexes = new ArrayList<>(); for (Target subTarget : getSubTargets(target)) { FieldIndex fieldIndex = getFieldIndex(subTarget); if (fieldIndex == null) { return null; } + indexes.add(Pair.create(subTarget, fieldIndex)); + } + for (Pair pair : indexes) { + Target subTarget = pair.first; + @NonNull FieldIndex fieldIndex = pair.second; @Nullable List arrayValues = subTarget.getArrayValues(fieldIndex); @Nullable Collection notInValues = subTarget.getNotInValues(fieldIndex); Bound lowerBound = subTarget.getLowerBound(fieldIndex); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/MutationBatch.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/MutationBatch.java index 7fb8d48c2ad..1b18b2090f4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/MutationBatch.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/model/mutation/MutationBatch.java @@ -142,7 +142,8 @@ public Map applyToLocalDocumentSet( for (DocumentKey key : getKeys()) { // TODO(mutabledocuments): This method should take a map of MutableDocuments and we should // remove this cast. - MutableDocument document = (MutableDocument) documentMap.get(key).getDocument(); + OverlayedDocument overlayedDocument = documentMap.get(key); + MutableDocument document = (MutableDocument) overlayedDocument.getDocument(); FieldMask mutatedFields = applyToLocalView(document, documentMap.get(key).getMutatedFields()); // Set mutationFields to null if the document is only from local mutations, this creates // a Set(or Delete) mutation, instead of trying to create a patch mutation as the overlay. @@ -151,6 +152,7 @@ public Map applyToLocalDocumentSet( if (overlay != null) { overlays.put(key, overlay); } + if (!document.isValidDocument()) { document.convertToNoDocument(SnapshotVersion.NONE); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index f916fee9683..57f51a7b8cc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -643,9 +643,7 @@ private List decodeFilters(StructuredQuery.Filter proto) { Filter result = decodeFilter(proto); // Instead of a singletonList containing AND(F1, F2, ...), we can return a list containing F1, - // F2, ... - // TODO(orquery): Once proper support for composite filters has been completed, we can remove - // this flattening from here. + // F2, ... to stay consistent with the older SDK versions. if (result instanceof com.google.firebase.firestore.core.CompositeFilter) { com.google.firebase.firestore.core.CompositeFilter compositeFilter = (com.google.firebase.firestore.core.CompositeFilter) result; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/TargetChange.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/TargetChange.java index 30cd7954cd0..9c8dad77ab5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/TargetChange.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/TargetChange.java @@ -31,9 +31,10 @@ public final class TargetChange { private final ImmutableSortedSet modifiedDocuments; private final ImmutableSortedSet removedDocuments; - public static TargetChange createSynthesizedTargetChangeForCurrentChange(boolean isCurrent) { + public static TargetChange createSynthesizedTargetChangeForCurrentChange( + boolean isCurrent, ByteString resumeToken) { return new TargetChange( - ByteString.EMPTY, + resumeToken, isCurrent, DocumentKey.emptyKeySet(), DocumentKey.emptyKeySet(), diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index c368022f07a..9eebe3ae92d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; +import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; @@ -243,6 +244,8 @@ public Thread newThread(@NonNull Runnable runnable) { } } + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") SynchronizedShutdownAwareExecutor() { DelayedStartFactory threadFactory = new DelayedStartFactory(); @@ -519,6 +522,9 @@ public void skipDelaysForTimerId(TimerId timerId) { */ public void panic(Throwable t) { executor.shutdownNow(); + + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") Handler handler = new Handler(Looper.getMainLooper()); handler.post( () -> { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 542b402c4fd..4fdbee103f0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -246,10 +246,7 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon context.errorPath, "Converting to Arrays is not supported, please use Lists instead"); } else if (clazz.getTypeParameters().length > 0) { throw deserializeError( - context.errorPath, - "Class " - + clazz.getName() - + " has generic type parameters, please use GenericTypeIndicator instead"); + context.errorPath, "Class " + clazz.getName() + " has generic type parameters"); } else if (clazz.equals(Object.class)) { return (T) o; } else if (clazz.isEnum()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/LogicUtils.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/LogicUtils.java index 616ce50730f..1f97ccc0703 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/LogicUtils.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/LogicUtils.java @@ -19,6 +19,8 @@ import com.google.firebase.firestore.core.CompositeFilter; import com.google.firebase.firestore.core.FieldFilter; import com.google.firebase.firestore.core.Filter; +import com.google.firebase.firestore.core.InFilter; +import com.google.firestore.v1.Value; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -286,6 +288,39 @@ protected static Filter computeDistributedNormalForm(Filter filter) { return runningResult; } + /** + * The `in` filter is only a syntactic sugar over a disjunction of equalities. For instance: `a in + * [1,2,3]` is in fact `a==1 || a==2 || a==3`. This method expands any `in` filter in the given + * input into a disjunction of equality filters and returns the expanded filter. + */ + protected static Filter computeInExpansion(Filter filter) { + assertFieldFilterOrCompositeFilter(filter); + + List expandedFilters = new ArrayList<>(); + + if (filter instanceof FieldFilter) { + if (filter instanceof InFilter) { + // We have reached a field filter with `in` operator. + for (Value value : ((InFilter) filter).getValue().getArrayValue().getValuesList()) { + expandedFilters.add( + FieldFilter.create( + ((InFilter) filter).getField(), FieldFilter.Operator.EQUAL, value)); + } + return new CompositeFilter(expandedFilters, CompositeFilter.Operator.OR); + } else { + // We have reached other kinds of field filters. + return filter; + } + } + + // We have a composite filter. + CompositeFilter compositeFilter = (CompositeFilter) filter; + for (Filter subfilter : compositeFilter.getFilters()) { + expandedFilters.add(computeInExpansion(subfilter)); + } + return new CompositeFilter(expandedFilters, compositeFilter.getOperator()); + } + /** * Given a composite filter, returns the list of terms in its disjunctive normal form. * @@ -302,7 +337,9 @@ public static List getDnfTerms(CompositeFilter filter) { return Collections.emptyList(); } - Filter result = computeDistributedNormalForm(filter); + // The `in` operator is a syntactic sugar over a disjunction of equalities. We should first + // replace such filters with equality filters before running the DNF transform. + Filter result = computeDistributedNormalForm(computeInExpansion(filter)); hardAssert( isDisjunctiveNormalForm(result), diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 6591db805c1..eba5e4e5500 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.util; +import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; @@ -206,6 +207,8 @@ public static String typeName(@Nullable Object obj) { } /** Raises an exception on Android's UI Thread and crashes the end user's app. */ + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public static void crashMainThread(RuntimeException exception) { new Handler(Looper.getMainLooper()) .post( diff --git a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java index a4bf3f7f3dd..d8a33a4f72b 100644 --- a/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java +++ b/firebase-firestore/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java @@ -83,7 +83,8 @@ public static QuerySnapshot querySnapshot( Map oldDocs, Map docsToAdd, boolean hasPendingWrites, - boolean isFromCache) { + boolean isFromCache, + boolean hasCachedResults) { DocumentSet oldDocuments = docSet(Document.KEY_COMPARATOR); ImmutableSortedSet mutatedKeys = DocumentKey.emptyKeySet(); for (Map.Entry pair : oldDocs.entrySet()) { @@ -116,7 +117,8 @@ public static QuerySnapshot querySnapshot( isFromCache, mutatedKeys, /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ false); + /* excludesMetadataChanges= */ false, + hasCachedResults); return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE); } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java index 4ce018def3e..649fa35bf59 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/QuerySnapshotTest.java @@ -56,21 +56,26 @@ public void testEquals() { ObjectValue firstValue = wrapObject("a", 1); ObjectValue secondValue = wrapObject("b", 1); - QuerySnapshot foo = TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, false); - QuerySnapshot fooDup = TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, false); + QuerySnapshot foo = + TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, false, false); + QuerySnapshot fooDup = + TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, false, false); QuerySnapshot differentPath = - TestUtil.querySnapshot("bar", map(), map("a", firstValue), true, false); + TestUtil.querySnapshot("bar", map(), map("a", firstValue), true, false, false); QuerySnapshot differentDoc = - TestUtil.querySnapshot("foo", map(), map("a", secondValue), true, false); + TestUtil.querySnapshot("foo", map(), map("a", secondValue), true, false, false); QuerySnapshot noPendingWrites = - TestUtil.querySnapshot("foo", map(), map("a", firstValue), false, false); + TestUtil.querySnapshot("foo", map(), map("a", firstValue), false, false, false); QuerySnapshot fromCache = - TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, true); + TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, true, false); + QuerySnapshot hasCachedResults = + TestUtil.querySnapshot("foo", map(), map("a", firstValue), true, false, true); assertEquals(foo, fooDup); assertNotEquals(foo, differentPath); assertNotEquals(foo, differentDoc); assertNotEquals(foo, noPendingWrites); assertNotEquals(foo, fromCache); + assertNotEquals(foo, hasCachedResults); // Note: `foo` and `differentDoc` have the same hash code since we no longer take document // contents into account. @@ -88,7 +93,8 @@ public void testToObjects() { ObjectValue objectData = ObjectValue.fromMap(map("timestamp", ServerTimestamps.valueOf(Timestamp.now(), null))); - QuerySnapshot foo = TestUtil.querySnapshot("foo", map(), map("a", objectData), true, false); + QuerySnapshot foo = + TestUtil.querySnapshot("foo", map(), map("a", objectData), true, false, false); List docs = foo.toObjects(POJO.class); assertEquals(1, docs.size()); @@ -126,7 +132,8 @@ public void testIncludeMetadataChanges() { /*isFromCache=*/ false, /*mutatedKeys=*/ keySet(), /*didSyncStateChange=*/ true, - /* excludesMetadataChanges= */ false); + /* excludesMetadataChanges= */ false, + /* hasCachedResults= */ false); QuerySnapshot snapshot = new QuerySnapshot(new Query(fooQuery, firestore), viewSnapshot, firestore); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/FilterTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/FilterTest.java index 5e14fdef546..0c5e55e2c51 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/FilterTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/FilterTest.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.testutil.TestUtil.andFilters; import static com.google.firebase.firestore.testutil.TestUtil.filter; import static com.google.firebase.firestore.testutil.TestUtil.orFilters; +import static com.google.firebase.firestore.testutil.TestUtil.query; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -56,7 +57,7 @@ public void testCompositeFilterMembers() { CompositeFilter orFilter = orFilters(A, B, C); assertTrue(orFilter.isDisjunction()); - assertEquals(andFilter.getFilters(), Arrays.asList(A, B, C)); + assertEquals(orFilter.getFilters(), Arrays.asList(A, B, C)); } @Test @@ -85,4 +86,11 @@ public void testCompositeFilterNestedChecks() { assertFalse(orFilter2.isFlat()); assertFalse(orFilter2.isFlatConjunction()); } + + @Test + public void testCanonicalIdOfFlatConjunctions() { + Target target1 = query("col").filter(A).filter(B).filter(C).toTarget(); + Target target2 = query("col").filter(andFilters(A, B, C)).toTarget(); + assertEquals(target1.getCanonicalId(), target2.getCanonicalId()); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java index dbc6e3ddc6a..31c1e9b0712 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryListenerTest.java @@ -108,7 +108,8 @@ public void testRaisesCollectionEvents() { snap2.isFromCache(), snap2.getMutatedKeys(), /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ false); + /* excludesMetadataChanges= */ false, + /* hasCachedResults= */ false); assertEquals(asList(snap2Prime), otherAccum); } @@ -262,7 +263,8 @@ public void testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChang snap4.isFromCache(), snap4.getMutatedKeys(), snap4.didSyncStateChange(), - /* excludeMetadataChanges= */ true); // This test excludes document metadata changes + /* excludeMetadataChanges= */ true, + /* hasCachedResults= */ false); // This test excludes document metadata changes assertEquals( asList( @@ -302,7 +304,9 @@ public void testMetadataOnlyDocumentChangesAreFilteredOut() { snap2.isFromCache(), snap2.getMutatedKeys(), snap2.didSyncStateChange(), - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); + assertEquals( asList(applyExpectedMetadata(snap1, MetadataChanges.EXCLUDE), expectedSnapshot2), filteredAccum); @@ -344,7 +348,8 @@ public void testWillWaitForSyncIfOnline() { /* isFromCache= */ false, snap3.getMutatedKeys(), /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); assertEquals(asList(expectedSnapshot), events); } @@ -382,7 +387,9 @@ public void testWillRaiseInitialEventWhenGoingOffline() { /* isFromCache= */ true, snap1.getMutatedKeys(), /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); + ViewSnapshot expectedSnapshot2 = new ViewSnapshot( snap2.getQuery(), @@ -392,7 +399,8 @@ public void testWillRaiseInitialEventWhenGoingOffline() { /* isFromCache= */ true, snap2.getMutatedKeys(), /* didSyncStateChange= */ false, - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); assertEquals(asList(expectedSnapshot1, expectedSnapshot2), events); } @@ -419,7 +427,8 @@ public void testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs() { /* isFromCache= */ true, snap1.getMutatedKeys(), /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); assertEquals(asList(expectedSnapshot), events); } @@ -445,7 +454,8 @@ public void testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs() { /* isFromCache= */ true, snap1.getMutatedKeys(), /* didSyncStateChange= */ true, - /* excludesMetadataChanges= */ true); + /* excludesMetadataChanges= */ true, + /* hasCachedResults= */ false); assertEquals(asList(expectedSnapshot), events); } @@ -458,6 +468,7 @@ private ViewSnapshot applyExpectedMetadata(ViewSnapshot snap, MetadataChanges me snap.isFromCache(), snap.getMutatedKeys(), snap.didSyncStateChange(), - MetadataChanges.EXCLUDE.equals(metadata)); + MetadataChanges.EXCLUDE.equals(metadata), + snap.hasCachedResults()); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java index 647601030ac..ee3920d3658 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/ViewSnapshotTest.java @@ -49,6 +49,7 @@ public void testConstructor() { boolean hasPendingWrites = true; boolean syncStateChanges = true; boolean excludesMetadataChanges = true; + boolean hasCachedResults = true; ViewSnapshot snapshot = new ViewSnapshot( @@ -59,7 +60,8 @@ public void testConstructor() { fromCache, mutatedKeys, syncStateChanges, - excludesMetadataChanges); + excludesMetadataChanges, + hasCachedResults); assertEquals(query, snapshot.getQuery()); assertEquals(docs, snapshot.getDocuments()); @@ -70,5 +72,6 @@ public void testConstructor() { assertEquals(hasPendingWrites, snapshot.hasPendingWrites()); assertEquals(syncStateChanges, snapshot.didSyncStateChange()); assertEquals(excludesMetadataChanges, snapshot.excludesMetadataChanges()); + assertEquals(hasCachedResults, snapshot.hasCachedResults()); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java index 20494aa46b1..b359489499a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java @@ -24,9 +24,14 @@ import com.google.firebase.firestore.model.MutableDocument; import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.SnapshotVersion; +import com.google.firebase.firestore.model.mutation.DeleteMutation; import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.Overlay; +import com.google.firebase.firestore.model.mutation.PatchMutation; +import com.google.firebase.firestore.model.mutation.SetMutation; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.SortedSet; @@ -35,10 +40,17 @@ * mutations read. */ class CountingQueryEngine extends QueryEngine { + enum OverlayType { + Patch, + Set, + Delete + } + private final QueryEngine queryEngine; private final int[] overlaysReadByCollection = new int[] {0}; private final int[] overlaysReadByKey = new int[] {0}; + private final Map overlayTypes = new HashMap(); private final int[] documentsReadByCollection = new int[] {0}; private final int[] documentsReadByKey = new int[] {0}; @@ -49,6 +61,7 @@ class CountingQueryEngine extends QueryEngine { void resetCounts() { overlaysReadByCollection[0] = 0; overlaysReadByKey[0] = 0; + overlayTypes.clear(); documentsReadByCollection[0] = 0; documentsReadByKey[0] = 0; } @@ -104,6 +117,14 @@ int getOverlaysReadByKey() { return overlaysReadByKey[0]; } + /** + * Returns the types of overlay returned by the OverlayCahce's `getOverlays()` API (since the last + * call to `resetCounts()`) + */ + Map getOverlayTypes() { + return Collections.unmodifiableMap(overlayTypes); + } + private RemoteDocumentCache wrapRemoteDocumentCache(RemoteDocumentCache subject) { return new RemoteDocumentCache() { @Override @@ -160,12 +181,19 @@ private DocumentOverlayCache wrapOverlayCache(DocumentOverlayCache subject) { @Override public Overlay getOverlay(DocumentKey key) { ++overlaysReadByKey[0]; - return subject.getOverlay(key); + Overlay overlay = subject.getOverlay(key); + overlayTypes.put(key, getOverlayType(overlay)); + return overlay; } public Map getOverlays(SortedSet keys) { overlaysReadByKey[0] += keys.size(); - return subject.getOverlays(keys); + Map overlays = subject.getOverlays(keys); + for (Map.Entry entry : overlays.entrySet()) { + overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue())); + } + + return overlays; } @Override @@ -182,6 +210,9 @@ public void removeOverlaysForBatchId(int batchId) { public Map getOverlays(ResourcePath collection, int sinceBatchId) { Map result = subject.getOverlays(collection, sinceBatchId); overlaysReadByCollection[0] += result.size(); + for (Map.Entry entry : result.entrySet()) { + overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue())); + } return result; } @@ -191,8 +222,23 @@ public Map getOverlays( Map result = subject.getOverlays(collectionGroup, sinceBatchId, count); overlaysReadByCollection[0] += result.size(); + for (Map.Entry entry : result.entrySet()) { + overlayTypes.put(entry.getKey(), getOverlayType(entry.getValue())); + } return result; } + + private OverlayType getOverlayType(Overlay overlay) { + if (overlay.getMutation() instanceof SetMutation) { + return OverlayType.Set; + } else if (overlay.getMutation() instanceof PatchMutation) { + return OverlayType.Patch; + } else if (overlay.getMutation() instanceof DeleteMutation) { + return OverlayType.Delete; + } else { + throw new IllegalStateException("Overlay is a unrecognizable mutation."); + } + } }; } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java index 0f42fc0854c..6f09b99bb13 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java @@ -24,6 +24,7 @@ import static com.google.firebase.firestore.testutil.TestUtil.existenceFilterEvent; import static com.google.firebase.firestore.testutil.TestUtil.filter; import static com.google.firebase.firestore.testutil.TestUtil.key; +import static com.google.firebase.firestore.testutil.TestUtil.keyMap; import static com.google.firebase.firestore.testutil.TestUtil.keySet; import static com.google.firebase.firestore.testutil.TestUtil.keys; import static com.google.firebase.firestore.testutil.TestUtil.map; @@ -82,6 +83,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.stream.Collectors; @@ -332,6 +334,11 @@ protected void assertOverlaysRead(int byKey, int byCollection) { "Overlays read (by collection)", byCollection, queryEngine.getOverlaysReadByCollection()); } + /** Asserts the expected overlay types. */ + protected void assertOverlayTypes(Map expected) { + assertEquals("Overlays types", expected, queryEngine.getOverlayTypes()); + } + /** * Asserts the expected numbers of documents read by the RemoteDocumentCache since the last call * to `resetPersistenceStats()`. @@ -975,6 +982,7 @@ public void testReadsAllDocumentsForInitialCollectionQueries() { localStore.executeQuery(query, /* usePreviousResults= */ true); assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1); + assertOverlayTypes(keyMap("foo/bonk", CountingQueryEngine.OverlayType.Set)); } @Test @@ -1689,4 +1697,21 @@ public void testMultipleFieldPatchesOnLocalDocs() { assertChanged(doc("foo/bar", 0, map("likes", 1, "stars", 2)).setHasLocalMutations()); assertContains(doc("foo/bar", 0, map("likes", 1, "stars", 2)).setHasLocalMutations()); } + + @Test + public void testUpdateOnRemoteDocLeadsToUpdateOverlay() { + Query query = query("foo"); + allocateQuery(query); + + applyRemoteEvent(updateRemoteEvent(doc("foo/baz", 10, map("a", 1)), asList(2), emptyList())); + applyRemoteEvent(updateRemoteEvent(doc("foo/bar", 20, map()), asList(2), emptyList())); + writeMutation(patchMutation("foo/baz", map("b", 2))); + + resetPersistenceStats(); + + localStore.executeQuery(query, /* usePreviousResults= */ true); + assertRemoteDocumentsRead(/* byKey= */ 0, /* byCollection= */ 2); + assertOverlaysRead(/* byKey= */ 0, /* byCollection= */ 1); + assertOverlayTypes(keyMap("foo/baz", CountingQueryEngine.OverlayType.Patch)); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java index d3286e293f1..aedd1401c74 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java @@ -640,4 +640,206 @@ public void orQueryWithArrayMembership() throws Exception { expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); assertEquals(docSet(query2.comparator(), doc1, doc4, doc6), result2); } + + @Test + public void queryWithMultipleInsOnTheSameField() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", 0)); + MutableDocument doc2 = doc("coll/2", 1, map("b", 1)); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", 2)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", 3)); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + // a IN [1,2,3] && a IN [0,1,4] should result in "a==1". + Query query1 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(1, 2, 3)), + filter("a", "in", Arrays.asList(0, 1, 4)))); + DocumentSet result1 = + expectFullCollectionScan(() -> runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query1.comparator(), doc1, doc4, doc5), result1); + + // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an empty set. + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("a", "in", Arrays.asList(0, 1, 4)))); + DocumentSet result2 = + expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query2.comparator()), result2); + + // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). + Query query3 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(0, 3)), + filter("a", "in", Arrays.asList(0, 2)))); + + DocumentSet result3 = + expectFullCollectionScan(() -> runQuery(query3, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query3.comparator(), doc3, doc6), result3); + } + + @Test + public void queryWithMultipleInsOnDifferentFields() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", 0)); + MutableDocument doc2 = doc("coll/2", 1, map("b", 1)); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", 2)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", 3)); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "in", Arrays.asList(0, 2)))); + DocumentSet result1 = + expectFullCollectionScan(() -> runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query1.comparator(), doc1, doc3, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "in", Arrays.asList(0, 2)))); + DocumentSet result2 = + expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query2.comparator(), doc3), result2); + } + + @Test + public void queryInWithArrayContainsAny() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + DocumentSet result1 = + expectFullCollectionScan(() -> runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query1.comparator(), doc1, doc3, doc4, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + + DocumentSet result2 = + expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query2.comparator(), doc3), result2); + + Query query3 = + query("coll") + .filter( + orFilters( + andFilters(filter("a", "in", Arrays.asList(2, 3)), filter("c", "==", 10)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + DocumentSet result3 = + expectFullCollectionScan(() -> runQuery(query3, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query3.comparator(), doc1, doc3, doc4), result3); + + Query query4 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + orFilters( + filter("b", "array-contains-any", Arrays.asList(0, 7)), + filter("c", "==", 20)))); + DocumentSet result4 = + expectFullCollectionScan(() -> runQuery(query4, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query4.comparator(), doc3, doc6), result4); + } + + @Test + public void queryInWithArrayContains() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), filter("b", "array-contains", 3))); + DocumentSet result1 = + expectFullCollectionScan(() -> runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query1.comparator(), doc3, doc4, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), filter("b", "array-contains", 7))); + + DocumentSet result2 = + expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query2.comparator(), doc3), result2); + + Query query3 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + andFilters(filter("b", "array-contains", 3), filter("a", "==", 1)))); + DocumentSet result3 = + expectFullCollectionScan(() -> runQuery(query3, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query3.comparator(), doc3, doc4, doc6), result3); + + Query query4 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + orFilters(filter("b", "array-contains", 7), filter("a", "==", 1)))); + DocumentSet result4 = + expectFullCollectionScan(() -> runQuery(query4, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query4.comparator(), doc3), result4); + } + + @Test + public void orderByEquality() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + + Query query1 = query("coll").filter(filter("a", "==", 1)).orderBy(orderBy("a")); + DocumentSet result1 = + expectFullCollectionScan(() -> runQuery(query1, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query1.comparator(), doc1, doc4, doc5), result1); + + Query query2 = + query("coll").filter(filter("a", "in", Arrays.asList(2, 3))).orderBy(orderBy("a")); + DocumentSet result2 = + expectFullCollectionScan(() -> runQuery(query2, MISSING_LAST_LIMBO_FREE_SNAPSHOT)); + assertEquals(docSet(query2.comparator(), doc6, doc3), result2); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java index 5a6fb8954de..792c5e8f0ce 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteLocalStoreTest.java @@ -22,6 +22,7 @@ import static com.google.firebase.firestore.testutil.TestUtil.fieldIndex; import static com.google.firebase.firestore.testutil.TestUtil.filter; import static com.google.firebase.firestore.testutil.TestUtil.key; +import static com.google.firebase.firestore.testutil.TestUtil.keyMap; import static com.google.firebase.firestore.testutil.TestUtil.map; import static com.google.firebase.firestore.testutil.TestUtil.orderBy; import static com.google.firebase.firestore.testutil.TestUtil.query; @@ -209,6 +210,12 @@ public void testUsesPartiallyIndexedOverlaysWhenAvailable() { Query query = query("coll").filter(filter("matches", "==", true)); executeQuery(query); assertOverlaysRead(/* byKey= */ 1, /* byCollection= */ 1); + assertOverlayTypes( + keyMap( + "coll/a", + CountingQueryEngine.OverlayType.Set, + "coll/b", + CountingQueryEngine.OverlayType.Set)); assertQueryReturned("coll/a", "coll/b"); } @@ -238,6 +245,7 @@ public void testDoesNotUseLimitWhenIndexIsOutdated() { // The query engine first reads the documents by key and then re-runs the query without limit. assertRemoteDocumentsRead(/* byKey= */ 5, /* byCollection= */ 0); assertOverlaysRead(/* byKey= */ 5, /* byCollection= */ 1); + assertOverlayTypes(keyMap("coll/b", CountingQueryEngine.OverlayType.Delete)); assertQueryReturned("coll/a", "coll/c"); } @@ -279,6 +287,7 @@ public void testIndexesServerTimestamps() { Query query = query("coll").orderBy(orderBy("time", "asc")); executeQuery(query); assertOverlaysRead(/* byKey= */ 1, /* byCollection= */ 0); + assertOverlayTypes(keyMap("coll/a", CountingQueryEngine.OverlayType.Set)); assertQueryReturned("coll/a"); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEngineTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEngineTest.java index f63e1c2a13d..06f1c80f879 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEngineTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEngineTest.java @@ -277,4 +277,265 @@ public void orQueryWithArrayMembershipUsingIndexes() throws Exception { expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); assertEquals(docSet(query2.comparator(), doc1, doc4, doc6), result2); } + + @Test + public void queryWithMultipleInsOnTheSameField() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", 0)); + MutableDocument doc2 = doc("coll/2", 1, map("b", 1)); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", 2)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", 3)); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.DESCENDING)); + indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5, doc6)); + indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc6)); + + // a IN [1,2,3] && a IN [0,1,4] should result in "a==1". + Query query1 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(1, 2, 3)), + filter("a", "in", Arrays.asList(0, 1, 4)))); + DocumentSet result1 = + expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE)); + assertEquals(docSet(query1.comparator(), doc1, doc4, doc5), result1); + + // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an empty set. + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("a", "in", Arrays.asList(0, 1, 4)))); + + DocumentSet result2 = + expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); + assertEquals(docSet(query2.comparator()), result2); + + // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). + Query query3 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(0, 3)), + filter("a", "in", Arrays.asList(0, 2)))); + + DocumentSet result3 = + expectOptimizedCollectionScan(() -> runQuery(query3, SnapshotVersion.NONE)); + assertEquals(docSet(query3.comparator(), doc3, doc6), result3); + + // Nested composite filter: (a IN [0,1,2,3] && (a IN [0,2] || (b>1 && a IN [1,3])) + Query query4 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(0, 1, 2, 3)), + orFilters( + filter("a", "in", Arrays.asList(0, 2)), + andFilters(filter("b", ">=", 1), filter("a", "in", Arrays.asList(1, 3)))))); + + DocumentSet result4 = + expectOptimizedCollectionScan(() -> runQuery(query4, SnapshotVersion.NONE)); + assertEquals(docSet(query4.comparator(), doc3, doc4), result4); + } + + @Test + public void queryWithMultipleInsOnDifferentFields() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", 0)); + MutableDocument doc2 = doc("coll/2", 1, map("b", 1)); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", 2)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", 3)); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.DESCENDING)); + indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5, doc6)); + indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc6)); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "in", Arrays.asList(0, 2)))); + DocumentSet result1 = + expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE)); + assertEquals(docSet(query1.comparator(), doc1, doc3, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "in", Arrays.asList(0, 2)))); + + DocumentSet result2 = + expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); + assertEquals(docSet(query2.comparator(), doc3), result2); + + // Nested composite filter: (b in [0,3] && (b IN [1] || (b in [2,3] && a IN [1,3])) + Query query3 = + query("coll") + .filter( + andFilters( + filter("b", "in", Arrays.asList(0, 3)), + orFilters( + filter("b", "in", Arrays.asList(1)), + andFilters( + filter("b", "in", Arrays.asList(2, 3)), + filter("a", "in", Arrays.asList(1, 3)))))); + + DocumentSet result3 = + expectOptimizedCollectionScan(() -> runQuery(query3, SnapshotVersion.NONE)); + assertEquals(docSet(query3.comparator(), doc4), result3); + } + + @Test + public void queryInWithArrayContainsAny() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.CONTAINS)); + indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5, doc6)); + indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc6)); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + DocumentSet result1 = + expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE)); + assertEquals(docSet(query1.comparator(), doc1, doc3, doc4, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + + DocumentSet result2 = + expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); + assertEquals(docSet(query2.comparator(), doc3), result2); + + Query query3 = + query("coll") + .filter( + orFilters( + andFilters(filter("a", "in", Arrays.asList(2, 3)), filter("c", "==", 10)), + filter("b", "array-contains-any", Arrays.asList(0, 7)))); + DocumentSet result3 = + expectOptimizedCollectionScan(() -> runQuery(query3, SnapshotVersion.NONE)); + assertEquals(docSet(query3.comparator(), doc1, doc3, doc4), result3); + + Query query4 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + orFilters( + filter("b", "array-contains-any", Arrays.asList(0, 7)), + filter("c", "==", 20)))); + DocumentSet result4 = + expectOptimizedCollectionScan(() -> runQuery(query4, SnapshotVersion.NONE)); + assertEquals(docSet(query4.comparator(), doc3, doc6), result4); + } + + @Test + public void queryInWithArrayContains() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.CONTAINS)); + indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5, doc6)); + indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc6)); + + Query query1 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), filter("b", "array-contains", 3))); + DocumentSet result1 = + expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE)); + assertEquals(docSet(query1.comparator(), doc3, doc4, doc6), result1); + + Query query2 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), filter("b", "array-contains", 7))); + + DocumentSet result2 = + expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); + assertEquals(docSet(query2.comparator(), doc3), result2); + + Query query3 = + query("coll") + .filter( + orFilters( + filter("a", "in", Arrays.asList(2, 3)), + andFilters(filter("b", "array-contains", 3), filter("a", "==", 1)))); + DocumentSet result3 = + expectOptimizedCollectionScan(() -> runQuery(query3, SnapshotVersion.NONE)); + assertEquals(docSet(query3.comparator(), doc3, doc4, doc6), result3); + + Query query4 = + query("coll") + .filter( + andFilters( + filter("a", "in", Arrays.asList(2, 3)), + orFilters(filter("b", "array-contains", 7), filter("a", "==", 1)))); + DocumentSet result4 = + expectOptimizedCollectionScan(() -> runQuery(query4, SnapshotVersion.NONE)); + assertEquals(docSet(query4.comparator(), doc3), result4); + } + + @Test + public void orderByEquality() throws Exception { + MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", Arrays.asList(0))); + MutableDocument doc2 = doc("coll/2", 1, map("b", Arrays.asList(1))); + MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", Arrays.asList(2, 7), "c", 10)); + MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", Arrays.asList(3, 7))); + MutableDocument doc5 = doc("coll/5", 1, map("a", 1)); + MutableDocument doc6 = doc("coll/6", 1, map("a", 2, "c", 20)); + addDocument(doc1, doc2, doc3, doc4, doc5, doc6); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING)); + indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.CONTAINS)); + indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5, doc6)); + indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc6)); + + Query query1 = query("coll").filter(filter("a", "==", 1)).orderBy(orderBy("a")); + DocumentSet result1 = + expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE)); + assertEquals(docSet(query1.comparator(), doc1, doc4, doc5), result1); + + Query query2 = + query("coll").filter(filter("a", "in", Arrays.asList(2, 3))).orderBy(orderBy("a")); + DocumentSet result2 = + expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE)); + assertEquals(docSet(query2.comparator(), doc6, doc3), result2); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/LogicUtilsTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/LogicUtilsTest.java index e48a8f92f53..89b8ca99ee3 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/LogicUtilsTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/LogicUtilsTest.java @@ -21,6 +21,7 @@ import static com.google.firebase.firestore.util.LogicUtils.applyAssociation; import static com.google.firebase.firestore.util.LogicUtils.applyDistribution; import static com.google.firebase.firestore.util.LogicUtils.computeDistributedNormalForm; +import static com.google.firebase.firestore.util.LogicUtils.computeInExpansion; import static com.google.firebase.firestore.util.LogicUtils.getDnfTerms; import static org.junit.Assert.assertEquals; @@ -275,4 +276,79 @@ public void testComputeDnf8() { assertThat(computeDistributedNormalForm(compositeFilter)).isEqualTo(expectedResult); assertThat(getDnfTerms(compositeFilter)).isEqualTo(Arrays.asList(expectedDnfTerms)); } + + @Test + public void testInExpansionForFieldFilters() { + FieldFilter input1 = filter("a", "in", Arrays.asList(1, 2, 3)); + FieldFilter input2 = filter("a", "<", 1); + FieldFilter input3 = filter("a", "<=", 1); + FieldFilter input4 = filter("a", "==", 1); + FieldFilter input5 = filter("a", "!=", 1); + FieldFilter input6 = filter("a", ">", 1); + FieldFilter input7 = filter("a", ">=", 1); + FieldFilter input8 = filter("a", "array-contains", 1); + FieldFilter input9 = filter("a", "array-contains-any", Arrays.asList(1, 2)); + FieldFilter input10 = filter("a", "not-in", Arrays.asList(1, 2)); + + assertThat(computeInExpansion(input1)) + .isEqualTo(orFilters(filter("a", "==", 1), filter("a", "==", 2), filter("a", "==", 3))); + + // Other operators should remain the same + assertThat(computeInExpansion(input2)).isEqualTo(input2); + assertThat(computeInExpansion(input3)).isEqualTo(input3); + assertThat(computeInExpansion(input4)).isEqualTo(input4); + assertThat(computeInExpansion(input5)).isEqualTo(input5); + assertThat(computeInExpansion(input6)).isEqualTo(input6); + assertThat(computeInExpansion(input7)).isEqualTo(input7); + assertThat(computeInExpansion(input8)).isEqualTo(input8); + assertThat(computeInExpansion(input9)).isEqualTo(input9); + assertThat(computeInExpansion(input10)).isEqualTo(input10); + } + + @Test + public void testInExpansionForCompositeFilters() { + CompositeFilter cf1 = + andFilters(filter("a", "==", 1), filter("b", "in", Arrays.asList(2, 3, 4))); + + assertThat(computeInExpansion(cf1)) + .isEqualTo( + andFilters( + filter("a", "==", 1), + orFilters(filter("b", "==", 2), filter("b", "==", 3), filter("b", "==", 4)))); + + CompositeFilter cf2 = + orFilters(filter("a", "==", 1), filter("b", "in", Arrays.asList(2, 3, 4))); + + assertThat(computeInExpansion(cf2)) + .isEqualTo( + orFilters( + filter("a", "==", 1), + orFilters(filter("b", "==", 2), filter("b", "==", 3), filter("b", "==", 4)))); + + CompositeFilter cf3 = + andFilters( + filter("a", "==", 1), + orFilters(filter("b", "==", 2), filter("c", "in", Arrays.asList(2, 3, 4)))); + + assertThat(computeInExpansion(cf3)) + .isEqualTo( + andFilters( + filter("a", "==", 1), + orFilters( + filter("b", "==", 2), + orFilters(filter("c", "==", 2), filter("c", "==", 3), filter("c", "==", 4))))); + + CompositeFilter cf4 = + orFilters( + filter("a", "==", 1), + andFilters(filter("b", "==", 2), filter("c", "in", Arrays.asList(2, 3, 4)))); + + assertThat(computeInExpansion(cf4)) + .isEqualTo( + orFilters( + filter("a", "==", 1), + andFilters( + filter("b", "==", 2), + orFilters(filter("c", "==", 2), filter("c", "==", 3), filter("c", "==", 4))))); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java index 6fffd89f3ec..c0ea8bfd405 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java @@ -1883,16 +1883,14 @@ public void primitiveClassesCanBePassedInTopLevel() { @Test public void passingInListTopLevelThrows() { assertExceptionContains( - "Class java.util.List has generic type parameters, please use GenericTypeIndicator " - + "instead", + "Class java.util.List has generic type parameters", () -> convertToCustomClass(Collections.singletonList("foo"), List.class)); } @Test public void passingInMapTopLevelThrows() { assertExceptionContains( - "Class java.util.Map has generic type parameters, please use GenericTypeIndicator " - + "instead", + "Class java.util.Map has generic type parameters", () -> convertToCustomClass(Collections.singletonMap("foo", "bar"), Map.class)); } @@ -1920,7 +1918,7 @@ public void passingInByteTopLevelThrows() { public void passingInGenericBeanTopLevelThrows() { assertExceptionContains( "Class com.google.firebase.firestore.util.MapperTest$GenericBean has generic type " - + "parameters, please use GenericTypeIndicator instead", + + "parameters", () -> deserialize("{'value': 'foo'}", GenericBean.class)); } diff --git a/firebase-firestore/src/test/resources/json/bundle_spec_test.json b/firebase-firestore/src/test/resources/json/bundle_spec_test.json index 1e21bbea06a..e45f01e937a 100644 --- a/firebase-firestore/src/test/resources/json/bundle_spec_test.json +++ b/firebase-firestore/src/test/resources/json/bundle_spec_test.json @@ -1386,7 +1386,7 @@ "value": { "value": "patched" }, - "version": 250 + "version": 0 } ], "query": { @@ -1670,7 +1670,7 @@ "value": { "value": "patched" }, - "version": 250 + "version": 0 } ], "query": { diff --git a/firebase-firestore/src/test/resources/json/existence_filter_spec_test.json b/firebase-firestore/src/test/resources/json/existence_filter_spec_test.json index 5798b877996..3083b762224 100644 --- a/firebase-firestore/src/test/resources/json/existence_filter_spec_test.json +++ b/firebase-firestore/src/test/resources/json/existence_filter_spec_test.json @@ -1,4 +1,240 @@ { + "Existence filter clears resume token": { + "describeName": "Existence Filters:", + "itName": "Existence filter clears resume token", + "tags": [ + "durable-persistence" + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/1", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + }, + { + "key": "collection/2", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 2 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/1", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + }, + { + "key": "collection/2", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 2 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "restart": true, + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/1", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 + }, + { + "key": "collection/2", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 2 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + } + ] + }, "Existence filter handled at global snapshot": { "describeName": "Existence Filters:", "itName": "Existence filter handled at global snapshot", @@ -1072,242 +1308,6 @@ } ] }, - "Existence filter mismatch invalidates index-free query": { - "describeName": "Existence Filters:", - "itName": "Existence filter clears resume token", - "tags": [ - "durable-persistence" - ], - "config": { - "numClients": 1, - "useGarbageCollection": true - }, - "steps": [ - { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } - } - }, - { - "watchAck": [ - 2 - ] - }, - { - "watchEntity": { - "docs": [ - { - "key": "collection/1", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 1 - }, - "version": 1000 - }, - { - "key": "collection/2", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 2 - }, - "version": 1000 - } - ], - "targets": [ - 2 - ] - } - }, - { - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 1000 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/1", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 1 - }, - "version": 1000 - }, - { - "key": "collection/2", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 2 - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - }, - { - "watchFilter": [ - [ - 2 - ], - "collection/1" - ] - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - }, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - }, - { - "restart": true, - "expectedState": { - "activeLimboDocs": [ - ], - "activeTargets": { - }, - "enqueuedLimboDocs": [ - ] - } - }, - { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/1", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 1 - }, - "version": 1000 - }, - { - "key": "collection/2", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": 2 - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } - } - } - ] - }, "Existence filter mismatch triggers re-run of query": { "describeName": "Existence Filters:", "itName": "Existence filter mismatch triggers re-run of query", diff --git a/firebase-firestore/src/test/resources/json/index_spec_test.json b/firebase-firestore/src/test/resources/json/index_spec_test.json new file mode 100644 index 00000000000..885b3357fed --- /dev/null +++ b/firebase-firestore/src/test/resources/json/index_spec_test.json @@ -0,0 +1,252 @@ +{ + "Index Creation succeeds even if not primary": { + "describeName": "Client Side Index", + "itName": "Index Creation succeeds even if not primary", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "isPrimary": false + } + }, + { + "clientIndex": 1, + "setIndexConfiguration": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": "price", + "order": "ASCENDING" + } + ], + "queryScope": "COLLECTION" + } + ] + }, + "expectedState": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": { + "len": 1, + "offset": 0, + "segments": [ + "price" + ] + }, + "kind": 0 + } + ], + "indexId": -1, + "indexState": { + "offset": { + "documentKey": { + "path": { + "len": 0, + "offset": 0, + "segments": [ + ] + } + }, + "largestBatchId": -1, + "readTime": { + "timestamp": { + "nanoseconds": 0, + "seconds": 0 + } + } + }, + "sequenceNumber": 0 + } + } + ] + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": { + "len": 1, + "offset": 0, + "segments": [ + "price" + ] + }, + "kind": 0 + } + ], + "indexId": -1, + "indexState": { + "offset": { + "documentKey": { + "path": { + "len": 0, + "offset": 0, + "segments": [ + ] + } + }, + "largestBatchId": -1, + "readTime": { + "timestamp": { + "nanoseconds": 0, + "seconds": 0 + } + } + }, + "sequenceNumber": 0 + } + } + ] + } + } + ] + }, + "Index Creation visible on all clients": { + "describeName": "Client Side Index", + "itName": "Index Creation visible on all clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 0, + "setIndexConfiguration": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": "price", + "order": "ASCENDING" + } + ], + "queryScope": "COLLECTION" + } + ] + }, + "expectedState": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": { + "len": 1, + "offset": 0, + "segments": [ + "price" + ] + }, + "kind": 0 + } + ], + "indexId": -1, + "indexState": { + "offset": { + "documentKey": { + "path": { + "len": 0, + "offset": 0, + "segments": [ + ] + } + }, + "largestBatchId": -1, + "readTime": { + "timestamp": { + "nanoseconds": 0, + "seconds": 0 + } + } + }, + "sequenceNumber": 0 + } + } + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "indexes": [ + { + "collectionGroup": "restaurants", + "fields": [ + { + "fieldPath": { + "len": 1, + "offset": 0, + "segments": [ + "price" + ] + }, + "kind": 0 + } + ], + "indexId": -1, + "indexState": { + "offset": { + "documentKey": { + "path": { + "len": 0, + "offset": 0, + "segments": [ + ] + } + }, + "largestBatchId": -1, + "readTime": { + "timestamp": { + "nanoseconds": 0, + "seconds": 0 + } + } + }, + "sequenceNumber": 0 + } + } + ], + "isPrimary": false + } + } + ] + } +} diff --git a/firebase-firestore/src/test/resources/json/limbo_spec_test.json b/firebase-firestore/src/test/resources/json/limbo_spec_test.json index 57f57e909e5..0b6abe08a2b 100644 --- a/firebase-firestore/src/test/resources/json/limbo_spec_test.json +++ b/firebase-firestore/src/test/resources/json/limbo_spec_test.json @@ -2627,7 +2627,7 @@ "matches": true, "modified": true }, - "version": 1000 + "version": 0 } ], "query": { @@ -2690,7 +2690,7 @@ "matches": true, "modified": true }, - "version": 1000 + "version": 0 } ], "errorCode": 0, diff --git a/firebase-firestore/src/test/resources/json/listen_spec_test.json b/firebase-firestore/src/test/resources/json/listen_spec_test.json index 1af1ab6594a..5755019b048 100644 --- a/firebase-firestore/src/test/resources/json/listen_spec_test.json +++ b/firebase-firestore/src/test/resources/json/listen_spec_test.json @@ -2257,19 +2257,16 @@ } ] }, - "Ensure correct query results with latency-compensated deletes": { + "Empty initial snapshot is raised from cache": { "describeName": "Listens:", - "itName": "Ensure correct query results with latency-compensated deletes", + "itName": "Empty initial snapshot is raised from cache", "tags": [ ], "config": { "numClients": 1, - "useGarbageCollection": true + "useGarbageCollection": false }, "steps": [ - { - "userDelete": "collection/b" - }, { "userListen": { "query": { @@ -2306,28 +2303,6 @@ { "watchEntity": { "docs": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "a": true - }, - "version": 1000 - }, - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "b": true - }, - "version": 1000 - } ], "targets": [ 2 @@ -2350,19 +2325,6 @@ }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "a": true - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -2376,42 +2338,48 @@ } ] }, + { + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, { "userListen": { "query": { "filters": [ ], - "limit": 10, - "limitType": "LimitToFirst", "orderBys": [ ], "path": "collection" }, - "targetId": 4 + "targetId": 2 }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "a": true - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ ], - "limit": 10, - "limitType": "LimitToFirst", "orderBys": [ ], "path": "collection" @@ -2430,38 +2398,73 @@ "path": "collection" } ], - "resumeToken": "" - }, - "4": { - "queries": [ - { - "filters": [ - ], - "limit": 10, - "limitType": "LimitToFirst", - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } } } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] } ] }, - "Ignores update from inactive target": { + "Empty initial snapshot is raised from cache in multiple tabs": { "describeName": "Listens:", - "itName": "Ignores update from inactive target", + "itName": "Empty initial snapshot is raised from cache in multiple tabs", "tags": [ + "multi-client" ], "config": { - "numClients": 1, + "numClients": 2, "useGarbageCollection": false }, "steps": [ { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -2490,24 +2493,15 @@ } }, { + "clientIndex": 0, "watchAck": [ 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } ], "targets": [ 2 @@ -2515,6 +2509,7 @@ } }, { + "clientIndex": 0, "watchCurrent": [ [ 2 @@ -2523,6 +2518,7 @@ ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], @@ -2530,19 +2526,6 @@ }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -2557,6 +2540,7 @@ ] }, { + "clientIndex": 0, "userUnlisten": [ 2, { @@ -2573,33 +2557,7 @@ } }, { - "watchEntity": { - "docs": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], - "targets": [ - 2 - ] - } - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } - }, - { + "clientIndex": 0, "watchRemove": { "targetIds": [ 2 @@ -2607,6 +2565,11 @@ } }, { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -2619,19 +2582,6 @@ }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false, @@ -2656,36 +2606,102 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } } } - } - ] - }, - "Individual (deleted) documents cannot revert": { - "describeName": "Listens:", - "itName": "Individual (deleted) documents cannot revert", - "tags": [ - ], - "config": { - "numClients": 1, - "useGarbageCollection": false - }, - "steps": [ + }, { - "userListen": { - "query": { - "filters": [ - [ - "visible", - "==", - true - ] - ], - "orderBys": [ - ], - "path": "collection" + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Empty-due-to-delete initial snapshot is raised from cache": { + "describeName": "Listens:", + "itName": "Empty-due-to-delete initial snapshot is raised from cache", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" }, "targetId": 2 }, @@ -2695,11 +2711,6 @@ "queries": [ { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -2726,8 +2737,7 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "v": 1 }, "version": 1000 } @@ -2761,8 +2771,7 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "v": 1 }, "version": 1000 } @@ -2772,11 +2781,6 @@ "hasPendingWrites": false, "query": { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -2790,11 +2794,6 @@ 2, { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -2813,6 +2812,9 @@ ] } }, + { + "userDelete": "collection/a" + }, { "userListen": { "query": { @@ -2822,24 +2824,10 @@ ], "path": "collection" }, - "targetId": 4 + "targetId": 2 }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": "v1000", - "visible": true - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false, @@ -2854,7 +2842,53 @@ ], "expectedState": { "activeTargets": { - "4": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + } + } + } + ] + }, + "Empty-due-to-delete initial snapshot is raised from cache in multiple tabs": { + "describeName": "Listens:", + "itName": "Empty-due-to-delete initial snapshot is raised from cache in multiple tabs", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { "queries": [ { "filters": [ @@ -2870,51 +2904,51 @@ } }, { + "clientIndex": 0, "watchAck": [ - 4 + 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ { "key": "collection/a", - "value": null, - "version": 3000 + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": 1 + }, + "version": 1000 } ], - "removedTargets": [ - 4 + "targets": [ + 2 ] } }, { + "clientIndex": 0, "watchCurrent": [ [ - 4 + 2 ], - "resume-token-4000" + "resume-token-1000" ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 4000 + "version": 1000 }, "expectedSnapshotEvents": [ { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "removed": [ + "added": [ { "key": "collection/a", "options": { @@ -2922,18 +2956,28 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "v": 1 }, "version": 1000 } - ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } } ] }, { + "clientIndex": 0, "userUnlisten": [ - 4, + 2, { "filters": [ ], @@ -2948,21 +2992,26 @@ } }, { + "clientIndex": 0, "watchRemove": { "targetIds": [ - 4 + 2 ] } }, { + "clientIndex": 0, + "userDelete": "collection/a" + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "userListen": { "query": { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -2970,130 +3019,43 @@ }, "targetId": 2 }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - [ - "visible", - "==", - true - ] - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "resume-token-1000" - } - } - } - }, - { - "watchAck": [ - 2 - ] - }, - { - "watchEntity": { - "docs": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "v": "v2000", - "visible": false - }, - "version": 2000 - } - ], - "removedTargets": [ - 2 - ] - } - }, - { - "watchCurrent": [ - [ - 2 - ], - "resume-token-5000" - ] - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 5000 - }, "expectedSnapshotEvents": [ { "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], "path": "collection" } } - ] - }, - { - "userUnlisten": [ - 2, - { - "filters": [ - [ - "visible", - "==", - true - ] - ], - "orderBys": [ - ], - "path": "collection" - } ], "expectedState": { "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } } } }, { - "watchRemove": { - "targetIds": [ - 2 - ] - } - }, - { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 4 - }, + "clientIndex": 0, + "drainQueue": true, "expectedState": { "activeTargets": { - "4": { + "2": { "queries": [ { "filters": [ @@ -3103,39 +3065,67 @@ "path": "collection" } ], - "resumeToken": "resume-token-4000" + "resumeToken": "resume-token-1000" } } } }, { + "clientIndex": 0, + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "clientIndex": 0, "watchAck": [ - 4 + 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ + { + "key": "collection/a", + "value": null, + "version": 2000 + } ], "targets": [ - 4 + 2 ] } }, { + "clientIndex": 0, "watchCurrent": [ [ - 4 + 2 ], - "resume-token-6000" + "resume-token-2000" ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 6000 - }, + "version": 2000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "errorCode": 0, @@ -3153,25 +3143,23 @@ } ] }, - "Individual documents cannot revert": { + "Ensure correct query results with latency-compensated deletes": { "describeName": "Listens:", - "itName": "Individual documents cannot revert", + "itName": "Ensure correct query results with latency-compensated deletes", "tags": [ ], "config": { "numClients": 1, - "useGarbageCollection": false + "useGarbageCollection": true }, "steps": [ + { + "userDelete": "collection/b" + }, { "userListen": { "query": { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -3185,11 +3173,6 @@ "queries": [ { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -3216,8 +3199,18 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "a": true + }, + "version": 1000 + }, + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "b": true }, "version": 1000 } @@ -3251,8 +3244,7 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "a": true }, "version": 1000 } @@ -3262,11 +3254,6 @@ "hasPendingWrites": false, "query": { "filters": [ - [ - "visible", - "==", - true - ] ], "orderBys": [ ], @@ -3275,39 +3262,13 @@ } ] }, - { - "userUnlisten": [ - 2, - { - "filters": [ - [ - "visible", - "==", - true - ] - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "expectedState": { - "activeTargets": { - } - } - }, - { - "watchRemove": { - "targetIds": [ - 2 - ] - } - }, { "userListen": { "query": { "filters": [ ], + "limit": 10, + "limitType": "LimitToFirst", "orderBys": [ ], "path": "collection" @@ -3324,8 +3285,7 @@ "hasLocalMutations": false }, "value": { - "v": "v1000", - "visible": true + "a": true }, "version": 1000 } @@ -3336,6 +3296,8 @@ "query": { "filters": [ ], + "limit": 10, + "limitType": "LimitToFirst", "orderBys": [ ], "path": "collection" @@ -3344,11 +3306,25 @@ ], "expectedState": { "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + }, "4": { "queries": [ { "filters": [ ], + "limit": 10, + "limitType": "LimitToFirst", "orderBys": [ ], "path": "collection" @@ -3358,10 +3334,50 @@ } } } - }, + } + ] + }, + "Ignores update from inactive target": { + "describeName": "Listens:", + "itName": "Ignores update from inactive target", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ { - "watchAck": [ - 4 + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 ] }, { @@ -3374,37 +3390,33 @@ "hasLocalMutations": false }, "value": { - "v": "v3000", - "visible": false + "key": "a" }, - "version": 3000 + "version": 1000 } ], "targets": [ - 4 + 2 ] } }, { "watchCurrent": [ [ - 4 + 2 ], - "resume-token-4000" + "resume-token-1000" ] }, { "watchSnapshot": { "targetIds": [ ], - "version": 4000 + "version": 1000 }, "expectedSnapshotEvents": [ { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "modified": [ + "added": [ { "key": "collection/a", "options": { @@ -3412,12 +3424,14 @@ "hasLocalMutations": false }, "value": { - "v": "v3000", - "visible": false + "key": "a" }, - "version": 3000 + "version": 1000 } ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, "query": { "filters": [ ], @@ -3430,7 +3444,7 @@ }, { "userUnlisten": [ - 4, + 2, { "filters": [ ], @@ -3444,13 +3458,107 @@ } } }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, { "watchRemove": { "targetIds": [ - 4 + 2 ] } }, + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + } + } + } + ] + }, + "Individual (deleted) documents cannot revert": { + "describeName": "Listens:", + "itName": "Individual (deleted) documents cannot revert", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ { "userListen": { "query": { @@ -3484,7 +3592,7 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } } } @@ -3504,13 +3612,13 @@ "hasLocalMutations": false }, "value": { - "v": "v2000", - "visible": false + "v": "v1000", + "visible": true }, - "version": 2000 + "version": 1000 } ], - "removedTargets": [ + "targets": [ 2 ] } @@ -3520,17 +3628,31 @@ [ 2 ], - "resume-token-5000" + "resume-token-1000" ] }, { "watchSnapshot": { "targetIds": [ ], - "version": 5000 + "version": 1000 }, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": "v1000", + "visible": true + }, + "version": 1000 + } + ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -3598,10 +3720,10 @@ "hasLocalMutations": false }, "value": { - "v": "v3000", - "visible": false + "v": "v1000", + "visible": true }, - "version": 3000 + "version": 1000 } ], "errorCode": 0, @@ -3628,7 +3750,7 @@ "path": "collection" } ], - "resumeToken": "resume-token-4000" + "resumeToken": "" } } } @@ -3641,8 +3763,13 @@ { "watchEntity": { "docs": [ + { + "key": "collection/a", + "value": null, + "version": 3000 + } ], - "targets": [ + "removedTargets": [ 4 ] } @@ -3652,14 +3779,14 @@ [ 4 ], - "resume-token-6000" + "resume-token-4000" ] }, { "watchSnapshot": { "targetIds": [ ], - "version": 6000 + "version": 4000 }, "expectedSnapshotEvents": [ { @@ -3672,67 +3799,110 @@ "orderBys": [ ], "path": "collection" - } + }, + "removed": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": "v1000", + "visible": true + }, + "version": 1000 + } + ] } ] - } - ] - }, - "Listen is established in new primary tab": { - "describeName": "Listens:", - "itName": "Listen is established in new primary tab", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 3, - "useGarbageCollection": false - }, - "steps": [ - { - "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "isPrimary": true - } }, { - "clientIndex": 0, - "userListen": { - "query": { + "userUnlisten": [ + 4, + { "filters": [ ], "orderBys": [ ], "path": "collection" - }, - "targetId": 2 - }, + } + ], "expectedState": { "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" + } + } + }, + { + "watchRemove": { + "targetIds": [ + 4 + ] + } + }, + { + "userListen": { + "query": { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } } } }, { - "clientIndex": 0, "watchAck": [ 2 ] }, { - "clientIndex": 0, "watchEntity": { "docs": [ { @@ -3742,52 +3912,43 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "v": "v2000", + "visible": false }, - "version": 1000 + "version": 2000 } ], - "targets": [ + "removedTargets": [ 2 ] } }, { - "clientIndex": 0, "watchCurrent": [ [ 2 ], - "resume-token-1000" + "resume-token-5000" ] }, { - "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 + "version": 5000 }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ + [ + "visible", + "==", + true + ] ], "orderBys": [ ], @@ -3797,15 +3958,34 @@ ] }, { - "clientIndex": 1, - "drainQueue": true + "userUnlisten": [ + 2, + { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } }, { - "clientIndex": 2, - "drainQueue": true + "watchRemove": { + "targetIds": [ + 2 + ] + } }, { - "clientIndex": 2, "userListen": { "query": { "filters": [ @@ -3814,25 +3994,12 @@ ], "path": "collection" }, - "targetId": 2 + "targetId": 4 }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -3845,7 +4012,7 @@ ], "expectedState": { "activeTargets": { - "2": { + "4": { "queries": [ { "filters": [ @@ -3855,72 +4022,123 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-4000" } } } }, { - "clientIndex": 0, - "drainQueue": true + "watchAck": [ + 4 + ] }, { - "clientIndex": 0, - "shutdown": true, - "expectedState": { - "activeLimboDocs": [ + "watchEntity": { + "docs": [ ], - "activeTargets": { - }, - "enqueuedLimboDocs": [ + "targets": [ + 4 ] } }, { - "clientIndex": 1, - "drainQueue": true + "watchCurrent": [ + [ + 4 + ], + "resume-token-6000" + ] }, { - "clientIndex": 1, - "runTimer": "client_metadata_refresh", + "watchSnapshot": { + "targetIds": [ + ], + "version": 6000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Individual documents cannot revert": { + "describeName": "Listens:", + "itName": "Individual documents cannot revert", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedState": { "activeTargets": { "2": { "queries": [ { "filters": [ + [ + "visible", + "==", + true + ] ], "orderBys": [ ], "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } - }, - "isPrimary": true + } } }, { - "clientIndex": 1, "watchAck": [ 2 ] }, { - "clientIndex": 1, "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "b" + "v": "v1000", + "visible": true }, - "version": 2000 + "version": 1000 } ], "targets": [ @@ -3929,38 +4147,33 @@ } }, { - "clientIndex": 1, "watchCurrent": [ [ 2 ], - "resume-token-2000" + "resume-token-1000" ] }, { - "clientIndex": 1, "watchSnapshot": { "targetIds": [ ], - "version": 2000 - } - }, - { - "clientIndex": 2, - "drainQueue": true, + "version": 1000 + }, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "b" + "v": "v1000", + "visible": true }, - "version": 2000 + "version": 1000 } ], "errorCode": 0, @@ -3968,6 +4181,11 @@ "hasPendingWrites": false, "query": { "filters": [ + [ + "visible", + "==", + true + ] ], "orderBys": [ ], @@ -3975,33 +4193,36 @@ } } ] - } - ] - }, - "Listen is established in newly started primary": { - "describeName": "Listens:", - "itName": "Listen is established in newly started primary", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 3, - "useGarbageCollection": false - }, - "steps": [ + }, { - "clientIndex": 0, - "drainQueue": true, + "userUnlisten": [ + 2, + { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], "expectedState": { - "isPrimary": true + "activeTargets": { + } } }, { - "clientIndex": 1, - "drainQueue": true + "watchRemove": { + "targetIds": [ + 2 + ] + } }, { - "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -4010,31 +4231,39 @@ ], "path": "collection" }, - "targetId": 2 + "targetId": 4 }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": "v1000", + "visible": true + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ ], - "resumeToken": "" + "orderBys": [ + ], + "path": "collection" } } - } - }, - { - "clientIndex": 0, - "drainQueue": true, + ], "expectedState": { "activeTargets": { - "2": { + "4": { "queries": [ { "filters": [ @@ -4050,13 +4279,11 @@ } }, { - "clientIndex": 0, "watchAck": [ - 2 + 4 ] }, { - "clientIndex": 0, "watchEntity": { "docs": [ { @@ -4066,39 +4293,37 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "v": "v3000", + "visible": false }, - "version": 1000 + "version": 3000 } ], "targets": [ - 2 + 4 ] } }, { - "clientIndex": 0, "watchCurrent": [ [ - 2 + 4 ], - "resume-token-1000" + "resume-token-4000" ] }, { - "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 - } - }, - { - "clientIndex": 1, - "drainQueue": true, + "version": 4000 + }, "expectedSnapshotEvents": [ { - "added": [ + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "modified": [ { "key": "collection/a", "options": { @@ -4106,14 +4331,12 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "v": "v3000", + "visible": false }, - "version": 1000 + "version": 3000 } ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, "query": { "filters": [ ], @@ -4125,30 +4348,74 @@ ] }, { - "clientIndex": 0, - "drainQueue": true - }, - { - "clientIndex": 0, - "shutdown": true, + "userUnlisten": [ + 4, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], "expectedState": { - "activeLimboDocs": [ - ], "activeTargets": { - }, - "enqueuedLimboDocs": [ + } + } + }, + { + "watchRemove": { + "targetIds": [ + 4 ] } }, { - "clientIndex": 2, - "drainQueue": true, + "userListen": { + "query": { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { "queries": [ { "filters": [ + [ + "visible", + "==", + true + ] ], "orderBys": [ ], @@ -4157,77 +4424,61 @@ ], "resumeToken": "resume-token-1000" } - }, - "isPrimary": true + } } }, { - "clientIndex": 2, "watchAck": [ 2 ] }, { - "clientIndex": 2, "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "b" + "v": "v2000", + "visible": false }, "version": 2000 } ], - "targets": [ + "removedTargets": [ 2 ] } }, { - "clientIndex": 2, "watchCurrent": [ [ 2 ], - "resume-token-2000" + "resume-token-5000" ] }, { - "clientIndex": 2, "watchSnapshot": { "targetIds": [ ], - "version": 2000 - } - }, - { - "clientIndex": 1, - "drainQueue": true, + "version": 5000 + }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ + [ + "visible", + "==", + true + ] ], "orderBys": [ ], @@ -4235,33 +4486,36 @@ } } ] - } - ] - }, - "Listen is re-listened to after primary tab failover": { - "describeName": "Listens:", - "itName": "Listen is re-listened to after primary tab failover", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 3, - "useGarbageCollection": false - }, - "steps": [ + }, { - "clientIndex": 0, - "drainQueue": true, + "userUnlisten": [ + 2, + { + "filters": [ + [ + "visible", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], "expectedState": { - "isPrimary": true + "activeTargets": { + } } }, { - "clientIndex": 1, - "drainQueue": true + "watchRemove": { + "targetIds": [ + 2 + ] + } }, { - "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -4270,11 +4524,39 @@ ], "path": "collection" }, - "targetId": 2 + "targetId": 4 }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "v": "v3000", + "visible": false + }, + "version": 3000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { - "2": { + "4": { "queries": [ { "filters": [ @@ -4284,14 +4566,86 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-4000" } } } }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-6000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 6000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Listen is established in new primary tab": { + "describeName": "Listens:", + "itName": "Listen is established in new primary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useGarbageCollection": false + }, + "steps": [ { "clientIndex": 0, "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedState": { "activeTargets": { "2": { @@ -4351,11 +4705,7 @@ "targetIds": [ ], "version": 1000 - } - }, - { - "clientIndex": 1, - "drainQueue": true, + }, "expectedSnapshotEvents": [ { "added": [ @@ -4384,6 +4734,10 @@ } ] }, + { + "clientIndex": 1, + "drainQueue": true + }, { "clientIndex": 2, "drainQueue": true @@ -4527,34 +4881,7 @@ "targetIds": [ ], "version": 2000 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] + } }, { "clientIndex": 2, @@ -4589,17 +4916,30 @@ } ] }, - "Listens are reestablished after network disconnect": { + "Listen is established in newly started primary": { "describeName": "Listens:", - "itName": "Listens are reestablished after network disconnect", + "itName": "Listen is established in newly started primary", "tags": [ + "multi-client" ], "config": { - "numClients": 1, - "useGarbageCollection": true + "numClients": 3, + "useGarbageCollection": false }, "steps": [ { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -4624,16 +4964,37 @@ ], "resumeToken": "" } - }, - "watchStreamRequestCount": 1 + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } } }, { + "clientIndex": 0, "watchAck": [ 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ { @@ -4654,6 +5015,7 @@ } }, { + "clientIndex": 0, "watchCurrent": [ [ 2 @@ -4662,11 +5024,16 @@ ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], "version": 1000 - }, + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ @@ -4696,21 +5063,12 @@ ] }, { - "enableNetwork": false, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "shutdown": true, "expectedState": { "activeLimboDocs": [ ], @@ -4721,7 +5079,8 @@ } }, { - "enableNetwork": true, + "clientIndex": 2, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -4737,15 +5096,17 @@ "resumeToken": "resume-token-1000" } }, - "watchStreamRequestCount": 2 + "isPrimary": true } }, { + "clientIndex": 2, "watchAck": [ 2 ] }, { + "clientIndex": 2, "watchEntity": { "docs": [ { @@ -4766,6 +5127,7 @@ } }, { + "clientIndex": 2, "watchCurrent": [ [ 2 @@ -4774,11 +5136,16 @@ ] }, { + "clientIndex": 2, "watchSnapshot": { "targetIds": [ ], "version": 2000 - }, + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ @@ -4809,9 +5176,9 @@ } ] }, - "Mirror queries from different secondary client": { + "Listen is re-listened to after primary tab failover": { "describeName": "Listens:", - "itName": "Mirror queries from different secondary client", + "itName": "Listen is re-listened to after primary tab failover", "tags": [ "multi-client" ], @@ -4822,76 +5189,22 @@ "steps": [ { "clientIndex": 0, - "drainQueue": true - }, - { - "applyClientState": { - "visibility": "visible" - }, - "clientIndex": 0 - }, - { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 1, - "userListen": { - "query": { - "filters": [ - ], - "limit": 2, - "limitType": "LimitToFirst", - "orderBys": [ - [ - "val", - "asc" - ] - ], - "path": "collection" - }, - "targetId": 2 - }, + "drainQueue": true, "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "limit": 2, - "limitType": "LimitToFirst", - "orderBys": [ - [ - "val", - "asc" - ] - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } + "isPrimary": true } }, { - "clientIndex": 2, + "clientIndex": 1, "drainQueue": true }, { - "clientIndex": 2, + "clientIndex": 1, "userListen": { "query": { "filters": [ ], - "limit": 2, - "limitType": "LimitToLast", "orderBys": [ - [ - "val", - "desc" - ] ], "path": "collection" }, @@ -4904,13 +5217,7 @@ { "filters": [ ], - "limit": 2, - "limitType": "LimitToLast", "orderBys": [ - [ - "val", - "desc" - ] ], "path": "collection" } @@ -4930,26 +5237,7 @@ { "filters": [ ], - "limit": 2, - "limitType": "LimitToLast", - "orderBys": [ - [ - "val", - "desc" - ] - ], - "path": "collection" - }, - { - "filters": [ - ], - "limit": 2, - "limitType": "LimitToFirst", "orderBys": [ - [ - "val", - "asc" - ] ], "path": "collection" } @@ -4976,18 +5264,7 @@ "hasLocalMutations": false }, "value": { - "val": 0 - }, - "version": 1000 - }, - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "val": 1 + "key": "a" }, "version": 1000 } @@ -5027,18 +5304,7 @@ "hasLocalMutations": false }, "value": { - "val": 0 - }, - "version": 1000 - }, - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "val": 1 + "key": "a" }, "version": 1000 } @@ -5049,13 +5315,7 @@ "query": { "filters": [ ], - "limit": 2, - "limitType": "LimitToFirst", "orderBys": [ - [ - "val", - "asc" - ] ], "path": "collection" } @@ -5064,21 +5324,23 @@ }, { "clientIndex": 2, - "drainQueue": true, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedSnapshotEvents": [ { "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "val": 1 - }, - "version": 1000 - }, { "key": "collection/a", "options": { @@ -5086,7 +5348,7 @@ "hasLocalMutations": false }, "value": { - "val": 0 + "key": "a" }, "version": 1000 } @@ -5097,45 +5359,52 @@ "query": { "filters": [ ], - "limit": 2, - "limitType": "LimitToLast", "orderBys": [ - [ - "val", - "desc" - ] ], "path": "collection" } } - ] - }, - { - "clientIndex": 2, - "userUnlisten": [ - 2, - { - "filters": [ - ], - "limit": 2, - "limitType": "LimitToLast", - "orderBys": [ - [ - "val", - "desc" - ] - ], - "path": "collection" - } ], "expectedState": { "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } } } }, { "clientIndex": 0, - "drainQueue": true, + "drainQueue": true + }, + { + "clientIndex": 0, + "shutdown": true, + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "runTimer": "client_metadata_refresh", "expectedState": { "activeTargets": { "2": { @@ -5143,34 +5412,35 @@ { "filters": [ ], - "limit": 2, - "limitType": "LimitToFirst", "orderBys": [ - [ - "val", - "asc" - ] ], "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } - } + }, + "isPrimary": true } }, { - "clientIndex": 0, + "clientIndex": 1, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 1, "watchEntity": { "docs": [ { - "key": "collection/c", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "val": 0 + "key": "b" }, "version": 2000 } @@ -5181,18 +5451,686 @@ } }, { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ + "clientIndex": 1, + "watchCurrent": [ + [ + 2 ], - "version": 2000 - } + "resume-token-2000" + ] }, { "clientIndex": 1, - "drainQueue": true, - "expectedSnapshotEvents": [ - { + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Listens are reestablished after network disconnect": { + "describeName": "Listens:", + "itName": "Listens are reestablished after network disconnect", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + }, + "watchStreamRequestCount": 1 + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "enableNetwork": false, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "enableNetwork": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + }, + "watchStreamRequestCount": 2 + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + } + ] + }, + "Mirror queries from different secondary client": { + "describeName": "Listens:", + "itName": "Mirror queries from different secondary client", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "val", + "asc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "val", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "val", + "desc" + ] + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "val", + "desc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "val", + "desc" + ] + ], + "path": "collection" + }, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "val", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 0 + }, + "version": 1000 + }, + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 1 + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 0 + }, + "version": 1000 + }, + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 1 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "val", + "asc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 1 + }, + "version": 1000 + }, + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 0 + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "val", + "desc" + ] + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToLast", + "orderBys": [ + [ + "val", + "desc" + ] + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "limit": 2, + "limitType": "LimitToFirst", + "orderBys": [ + [ + "val", + "asc" + ] + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "val": 0 + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { "added": [ { "key": "collection/c", @@ -6414,16 +7352,294 @@ { "clientIndex": 0, "drainQueue": true, - "expectedState": { - "activeTargets": { + "expectedState": { + "activeTargets": { + } + } + } + ] + }, + "New client becomes primary if no client has its network enabled": { + "describeName": "Listens:", + "itName": "New client becomes primary if no client has its network enabled", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "enableNetwork": false, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + }, + "isPrimary": true + } + }, + { + "clientIndex": 2, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 2, + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 2, + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "clientIndex": 2, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } } - } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] } ] }, - "New client becomes primary if no client has its network enabled": { + "New client uses existing online state": { "describeName": "Listens:", - "itName": "New client becomes primary if no client has its network enabled", + "itName": "New client uses existing online state", "tags": [ "multi-client" ], @@ -6516,6 +7732,18 @@ "clientIndex": 1, "drainQueue": true }, + { + "clientIndex": 1, + "enableNetwork": false, + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, { "clientIndex": 1, "userListen": { @@ -6590,8 +7818,21 @@ } }, { - "clientIndex": 1, - "drainQueue": true, + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedSnapshotEvents": [ { "errorCode": 0, @@ -6605,11 +7846,7 @@ "path": "collection" } } - ] - }, - { - "clientIndex": 2, - "drainQueue": true, + ], "expectedState": { "activeTargets": { "2": { @@ -6622,52 +7859,27 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } - }, - "isPrimary": true - } - }, - { - "clientIndex": 2, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 2, - "watchEntity": { - "docs": [ - ], - "targets": [ - 2 - ] + } } }, { "clientIndex": 2, - "watchCurrent": [ - [ - 2 - ], - "resume-token-2000" - ] - }, - { - "clientIndex": 2, - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } - }, - { - "clientIndex": 0, - "drainQueue": true, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedSnapshotEvents": [ { "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -6677,36 +7889,34 @@ "path": "collection" } } - ] - }, - { - "clientIndex": 1, - "drainQueue": true, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } ], - "path": "collection" + "resumeToken": "" } } - ] + } } ] }, - "New client uses existing online state": { + "Offline state doesn't persist if primary is shut down": { "describeName": "Listens:", - "itName": "New client uses existing online state", + "itName": "Offline state doesn't persist if primary is shut down", "tags": [ "multi-client" ], "config": { - "numClients": 3, + "numClients": 2, "useGarbageCollection": false }, "steps": [ @@ -6745,40 +7955,11 @@ }, { "clientIndex": 0, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 0, - "watchEntity": { - "docs": [ - ], - "targets": [ - 2 - ] - } - }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, - { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 1000 - }, + "enableNetwork": false, "expectedSnapshotEvents": [ { "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -6788,15 +7969,19 @@ "path": "collection" } } - ] - }, - { - "clientIndex": 1, - "drainQueue": true + ], + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } }, { - "clientIndex": 1, - "enableNetwork": false, + "clientIndex": 0, + "shutdown": true, "expectedState": { "activeLimboDocs": [ ], @@ -6808,6 +7993,51 @@ }, { "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + } + ] + }, + "Omits global resume tokens for a short while": { + "describeName": "Listens:", + "itName": "Omits global resume tokens for a short while", + "tags": [ + "durable-persistence" + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ + { "userListen": { "query": { "filters": [ @@ -6818,20 +8048,6 @@ }, "targetId": 2 }, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], "expectedState": { "activeTargets": { "2": { @@ -6850,16 +8066,61 @@ } }, { - "clientIndex": 0, - "drainQueue": true + "watchAck": [ + 2 + ] }, { - "clientIndex": 0, - "enableNetwork": false, + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], "errorCode": 0, - "fromCache": true, + "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ @@ -6869,7 +8130,18 @@ "path": "collection" } } - ], + ] + }, + { + "watchSnapshot": { + "resumeToken": "resume-token-2000", + "targetIds": [ + ], + "version": 2000 + } + }, + { + "restart": true, "expectedState": { "activeLimboDocs": [ ], @@ -6880,11 +8152,6 @@ } }, { - "clientIndex": 2, - "drainQueue": true - }, - { - "clientIndex": 2, "userListen": { "query": { "filters": [ @@ -6897,6 +8164,19 @@ }, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false, @@ -6921,27 +8201,34 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } } } }, { - "clientIndex": 2, - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-3000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 3000 }, "expectedSnapshotEvents": [ { "errorCode": 0, - "fromCache": true, + "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ @@ -6951,43 +8238,22 @@ "path": "collection" } } - ], - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } - } + ] } ] }, - "Offline state doesn't persist if primary is shut down": { + "Persists global resume tokens if the snapshot is old enough": { "describeName": "Listens:", - "itName": "Offline state doesn't persist if primary is shut down", + "itName": "Persists global resume tokens if the snapshot is old enough", "tags": [ - "multi-client" + "durable-persistence" ], "config": { - "numClients": 2, + "numClients": 1, "useGarbageCollection": false }, "steps": [ { - "clientIndex": 0, - "drainQueue": true - }, - { - "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -7016,12 +8282,61 @@ } }, { - "clientIndex": 0, - "enableNetwork": false, + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], "errorCode": 0, - "fromCache": true, + "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ @@ -7031,19 +8346,18 @@ "path": "collection" } } - ], - "expectedState": { - "activeLimboDocs": [ + ] + }, + { + "watchSnapshot": { + "resumeToken": "resume-token-minutes-later", + "targetIds": [ ], - "activeTargets": { - }, - "enqueuedLimboDocs": [ - ] + "version": 300001000 } }, { - "clientIndex": 0, - "shutdown": true, + "restart": true, "expectedState": { "activeLimboDocs": [ ], @@ -7054,11 +8368,6 @@ } }, { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -7069,6 +8378,33 @@ }, "targetId": 2 }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { @@ -7081,18 +8417,51 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-minutes-later" } } } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-even-later" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 300002000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] } ] }, - "Omits global resume tokens for a short while": { + "Persists global resume tokens on unlisten": { "describeName": "Listens:", - "itName": "Omits global resume tokens for a short while", + "itName": "Persists global resume tokens on unlisten", "tags": [ - "durable-persistence" ], "config": { "numClients": 1, @@ -7203,13 +8572,25 @@ } }, { - "restart": true, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], "expectedState": { - "activeLimboDocs": [ - ], "activeTargets": { - }, - "enqueuedLimboDocs": [ + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 ] } }, @@ -7263,7 +8644,7 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "resume-token-2000" } } } @@ -7304,11 +8685,10 @@ } ] }, - "Persists global resume tokens if the snapshot is old enough": { + "Persists resume token sent with target": { "describeName": "Listens:", - "itName": "Persists global resume tokens if the snapshot is old enough", + "itName": "Persists resume token sent with target", "tags": [ - "durable-persistence" ], "config": { "numClients": 1, @@ -7348,6 +8728,44 @@ 2 ] }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, { "watchEntity": { "docs": [ @@ -7360,7 +8778,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "targets": [ @@ -7369,18 +8787,19 @@ } }, { - "watchCurrent": [ - [ + "watchSnapshot": { + "resumeToken": "resume-token-2000", + "targetIds": [ 2 ], - "resume-token-1000" - ] + "version": 2000 + } }, { "watchSnapshot": { "targetIds": [ ], - "version": 1000 + "version": 2000 }, "expectedSnapshotEvents": [ { @@ -7394,7 +8813,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "errorCode": 0, @@ -7411,21 +8830,25 @@ ] }, { - "watchSnapshot": { - "resumeToken": "resume-token-minutes-later", - "targetIds": [ - ], - "version": 300001000 + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } } }, { - "restart": true, - "expectedState": { - "activeLimboDocs": [ - ], - "activeTargets": { - }, - "enqueuedLimboDocs": [ + "watchRemove": { + "targetIds": [ + 2 ] } }, @@ -7452,7 +8875,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "errorCode": 0, @@ -7479,7 +8902,7 @@ "path": "collection" } ], - "resumeToken": "resume-token-minutes-later" + "resumeToken": "resume-token-2000" } } } @@ -7489,19 +8912,28 @@ 2 ] }, + { + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, { "watchCurrent": [ [ 2 ], - "resume-token-even-later" + "resume-token-3000" ] }, { "watchSnapshot": { "targetIds": [ ], - "version": 300002000 + "version": 3000 }, "expectedSnapshotEvents": [ { @@ -7520,27 +8952,106 @@ } ] }, - "Persists global resume tokens on unlisten": { + "Previous primary immediately regains primary lease": { "describeName": "Listens:", - "itName": "Persists global resume tokens on unlisten", + "itName": "Previous primary immediately regains primary lease", "tags": [ + "multi-client" ], "config": { - "numClients": 1, + "numClients": 2, "useGarbageCollection": false }, "steps": [ { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "applyClientState": { + "primary": true }, + "clientIndex": 1, "expectedState": { "activeTargets": { "2": { @@ -7553,17 +9064,20 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } - } + }, + "isPrimary": true } }, { + "clientIndex": 1, "watchAck": [ 2 ] }, { + "clientIndex": 1, "watchEntity": { "docs": [ { @@ -7575,7 +9089,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "targets": [ @@ -7584,89 +9098,44 @@ } }, { + "clientIndex": 1, "watchCurrent": [ [ 2 ], - "resume-token-1000" - ] - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 1000 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } + "resume-token-2000" ] }, { + "clientIndex": 1, "watchSnapshot": { - "resumeToken": "resume-token-2000", "targetIds": [ ], "version": 2000 } }, { - "userUnlisten": [ - 2, - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], + "clientIndex": 1, + "shutdown": true, "expectedState": { + "activeLimboDocs": [ + ], "activeTargets": { - } + }, + "enqueuedLimboDocs": [ + ] } }, { - "watchRemove": { - "targetIds": [ - 2 - ] + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "isPrimary": true } }, { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, + "clientIndex": 0, + "runTimer": "client_metadata_refresh", "expectedSnapshotEvents": [ { "added": [ @@ -7679,11 +9148,11 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "errorCode": 0, - "fromCache": true, + "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ @@ -7708,56 +9177,36 @@ ], "resumeToken": "resume-token-2000" } - } + }, + "isPrimary": true } - }, - { - "watchAck": [ - 2 - ] - }, - { - "watchCurrent": [ - [ - 2 - ], - "resume-token-3000" - ] - }, - { - "watchSnapshot": { - "targetIds": [ - ], - "version": 3000 - }, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] } ] }, - "Persists resume token sent with target": { + "Query bounces between primaries": { "describeName": "Listens:", - "itName": "Persists resume token sent with target", + "itName": "Query bounces between primaries", "tags": [ + "multi-client" ], "config": { - "numClients": 1, + "numClients": 3, "useGarbageCollection": false }, "steps": [ { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -7786,13 +9235,46 @@ } }, { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 1, "watchAck": [ 2 ] }, { + "clientIndex": 1, "watchEntity": { "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } ], "targets": [ 2 @@ -7800,6 +9282,7 @@ } }, { + "clientIndex": 1, "watchCurrent": [ [ 2 @@ -7808,13 +9291,31 @@ ] }, { + "clientIndex": 1, "watchSnapshot": { "targetIds": [ ], "version": 1000 - }, + } + }, + { + "clientIndex": 0, + "drainQueue": true, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -7829,16 +9330,65 @@ ] }, { + "clientIndex": 2, + "drainQueue": true + }, + { + "applyClientState": { + "primary": true + }, + "clientIndex": 2, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "resume-token-1000" + } + }, + "isPrimary": true + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "runTimer": "client_metadata_refresh", + "expectedState": { + "isPrimary": false + } + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 2, "watchEntity": { "docs": [ { - "key": "collection/a", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "a" + "key": "b" }, "version": 2000 } @@ -7849,31 +9399,36 @@ } }, { - "watchSnapshot": { - "resumeToken": "resume-token-2000", - "targetIds": [ + "clientIndex": 2, + "watchCurrent": [ + [ 2 ], - "version": 2000 - } + "resume-token-2000" + ] }, { + "clientIndex": 2, "watchSnapshot": { "targetIds": [ ], "version": 2000 - }, + } + }, + { + "clientIndex": 0, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/a", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "a" + "key": "b" }, "version": 2000 } @@ -7892,66 +9447,14 @@ ] }, { - "userUnlisten": [ - 2, - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "expectedState": { - "activeTargets": { - } - } - }, - { - "watchRemove": { - "targetIds": [ - 2 - ] - } + "clientIndex": 1, + "drainQueue": true }, { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 + "applyClientState": { + "primary": true }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], + "clientIndex": 1, "expectedState": { "activeTargets": { "2": { @@ -7966,17 +9469,31 @@ ], "resumeToken": "resume-token-2000" } - } + }, + "isPrimary": true } }, { + "clientIndex": 1, "watchAck": [ 2 ] }, { + "clientIndex": 1, "watchEntity": { "docs": [ + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "c" + }, + "version": 3000 + } ], "targets": [ 2 @@ -7984,6 +9501,7 @@ } }, { + "clientIndex": 1, "watchCurrent": [ [ 2 @@ -7992,13 +9510,31 @@ ] }, { + "clientIndex": 1, "watchSnapshot": { "targetIds": [ ], "version": 3000 - }, + } + }, + { + "clientIndex": 0, + "drainQueue": true, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "c" + }, + "version": 3000 + } + ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -8014,9 +9550,9 @@ } ] }, - "Previous primary immediately regains primary lease": { + "Query is executed by primary client": { "describeName": "Listens:", - "itName": "Previous primary immediately regains primary lease", + "itName": "Query is executed by primary client", "tags": [ "multi-client" ], @@ -8030,7 +9566,17 @@ "drainQueue": true }, { - "clientIndex": 0, + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -8060,60 +9606,7 @@ }, { "clientIndex": 0, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 0, - "watchEntity": { - "docs": [ - ], - "targets": [ - 2 - ] - } - }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, - { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 1000 - }, - "expectedSnapshotEvents": [ - { - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - }, - { - "clientIndex": 1, - "drainQueue": true - }, - { - "applyClientState": { - "primary": true - }, - "clientIndex": 1, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -8126,20 +9619,19 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } - }, - "isPrimary": true + } } }, { - "clientIndex": 1, + "clientIndex": 0, "watchAck": [ 2 ] }, { - "clientIndex": 1, + "clientIndex": 0, "watchEntity": { "docs": [ { @@ -8147,57 +9639,29 @@ "options": { "hasCommittedMutations": false, "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 2000 - } - ], - "targets": [ - 2 - ] - } - }, - { - "clientIndex": 1, - "watchCurrent": [ - [ - 2 - ], - "resume-token-2000" - ] - }, - { - "clientIndex": 1, - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } - }, - { - "clientIndex": 1, - "shutdown": true, - "expectedState": { - "activeLimboDocs": [ + }, + "value": { + "key": "a" + }, + "version": 1000 + } ], - "activeTargets": { - }, - "enqueuedLimboDocs": [ + "targets": [ + 2 ] } }, { "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "isPrimary": true + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 } }, { - "clientIndex": 0, - "runTimer": "client_metadata_refresh", + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ @@ -8210,11 +9674,11 @@ "value": { "key": "a" }, - "version": 2000 + "version": 1000 } ], "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -8224,51 +9688,73 @@ "path": "collection" } } - ], - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ ], - "resumeToken": "resume-token-2000" + "orderBys": [ + ], + "path": "collection" } - }, - "isPrimary": true - } + } + ] } ] }, - "Query bounces between primaries": { + "Query is joined by primary client": { "describeName": "Listens:", - "itName": "Query bounces between primaries", + "itName": "Query is joined by primary client", "tags": [ "multi-client" ], "config": { - "numClients": 3, + "numClients": 2, "useGarbageCollection": false }, "steps": [ { - "clientIndex": 1, + "clientIndex": 0, "drainQueue": true, "expectedState": { "isPrimary": true } }, { - "clientIndex": 0, + "clientIndex": 1, "drainQueue": true }, { - "clientIndex": 0, + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -8297,7 +9783,7 @@ } }, { - "clientIndex": 1, + "clientIndex": 0, "drainQueue": true, "expectedState": { "activeTargets": { @@ -8317,13 +9803,13 @@ } }, { - "clientIndex": 1, + "clientIndex": 0, "watchAck": [ 2 ] }, { - "clientIndex": 1, + "clientIndex": 0, "watchEntity": { "docs": [ { @@ -8344,24 +9830,24 @@ } }, { - "clientIndex": 1, + "clientIndex": 0, "watchCurrent": [ [ 2 ], - "resume-token-1000" + "resume-token-100" ] }, { - "clientIndex": 1, + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 + "version": 100 } }, { - "clientIndex": 0, + "clientIndex": 1, "drainQueue": true, "expectedSnapshotEvents": [ { @@ -8392,14 +9878,88 @@ ] }, { - "clientIndex": 2, + "clientIndex": 0, "drainQueue": true }, { - "applyClientState": { - "primary": true + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + } + }, + { + "clientIndex": 0, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 }, - "clientIndex": 2, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + }, + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { @@ -8412,73 +9972,69 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } - }, - "isPrimary": true - } - }, - { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 1, - "runTimer": "client_metadata_refresh", - "expectedState": { - "isPrimary": false + } } }, { - "clientIndex": 2, - "drainQueue": true - }, - { - "clientIndex": 2, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 2, + "clientIndex": 0, "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/c", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "b" + "key": "c" }, - "version": 2000 + "version": 3000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 3000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "c" + }, + "version": 3000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" } - ], - "targets": [ - 2 - ] - } - }, - { - "clientIndex": 2, - "watchCurrent": [ - [ - 2 - ], - "resume-token-2000" + } ] }, { - "clientIndex": 2, - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } - }, - { - "clientIndex": 0, + "clientIndex": 1, "drainQueue": true, "expectedSnapshotEvents": [ { @@ -8505,18 +10061,113 @@ ], "path": "collection" } + }, + { + "added": [ + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "c" + }, + "version": 3000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } } ] + } + ] + }, + "Query is rejected and re-listened to": { + "describeName": "Listens:", + "itName": "Query is rejected and re-listened to", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } }, { - "clientIndex": 1, - "drainQueue": true + "watchRemove": { + "cause": { + "code": 8 + }, + "targetIds": [ + 2 + ] + }, + "expectedSnapshotEvents": [ + { + "errorCode": 8, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + } + } }, { - "applyClientState": { - "primary": true + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 }, - "clientIndex": 1, "expectedState": { "activeTargets": { "2": { @@ -8529,33 +10180,19 @@ "path": "collection" } ], - "resumeToken": "resume-token-2000" + "resumeToken": "" } - }, - "isPrimary": true + } } }, { - "clientIndex": 1, "watchAck": [ 2 ] }, { - "clientIndex": 1, "watchEntity": { "docs": [ - { - "key": "collection/c", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "c" - }, - "version": 3000 - } ], "targets": [ 2 @@ -8563,40 +10200,21 @@ } }, { - "clientIndex": 1, "watchCurrent": [ [ 2 ], - "resume-token-3000" + "resume-token-1000" ] }, { - "clientIndex": 1, "watchSnapshot": { "targetIds": [ ], - "version": 3000 - } - }, - { - "clientIndex": 0, - "drainQueue": true, + "version": 1000 + }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/c", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "c" - }, - "version": 3000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -8612,9 +10230,9 @@ } ] }, - "Query is executed by primary client": { + "Query is rejected and re-listened to by secondary client": { "describeName": "Listens:", - "itName": "Query is executed by primary client", + "itName": "Query is rejected and re-listened to by secondary client", "tags": [ "multi-client" ], @@ -8688,37 +10306,17 @@ }, { "clientIndex": 0, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 0, - "watchEntity": { - "docs": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "targets": [ + "watchRemove": { + "cause": { + "code": 8 + }, + "targetIds": [ 2 ] - } - }, - { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 1000 + }, + "expectedState": { + "activeTargets": { + } } }, { @@ -8726,35 +10324,83 @@ "drainQueue": true, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": true, + "errorCode": 8, + "fromCache": false, "hasPendingWrites": false, "query": { "filters": [ ], - "orderBys": [ + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } ], - "path": "collection" + "resumeToken": "" } } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 ] }, { "clientIndex": 0, - "drainQueue": true + "watchEntity": { + "docs": [ + ], + "targets": [ + 2 + ] + } }, { "clientIndex": 0, @@ -8762,7 +10408,7 @@ [ 2 ], - "resume-token-2000" + "resume-token-1000" ] }, { @@ -8770,7 +10416,7 @@ "watchSnapshot": { "targetIds": [ ], - "version": 2000 + "version": 1000 } }, { @@ -8793,9 +10439,9 @@ } ] }, - "Query is joined by primary client": { + "Query is rejected by primary client": { "describeName": "Listens:", - "itName": "Query is joined by primary client", + "itName": "Query is rejected by primary client", "tags": [ "multi-client" ], @@ -8806,10 +10452,13 @@ "steps": [ { "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "isPrimary": true - } + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 }, { "clientIndex": 1, @@ -8866,46 +10515,17 @@ }, { "clientIndex": 0, - "watchAck": [ - 2 - ] - }, - { - "clientIndex": 0, - "watchEntity": { - "docs": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "targets": [ + "watchRemove": { + "cause": { + "code": 8 + }, + "targetIds": [ 2 ] - } - }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-100" - ] - }, - { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 100 + }, + "expectedState": { + "activeTargets": { + } } }, { @@ -8913,20 +10533,7 @@ "drainQueue": true, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "errorCode": 0, + "errorCode": 8, "fromCache": false, "hasPendingWrites": false, "query": { @@ -8938,42 +10545,36 @@ } } ] - }, + } + ] + }, + "Query is resumed by secondary client": { + "describeName": "Listens:", + "itName": "Query is resumed by secondary client", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useGarbageCollection": false + }, + "steps": [ { "clientIndex": 0, "drainQueue": true }, { - "clientIndex": 0, - "watchEntity": { - "docs": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], - "targets": [ - 2 - ] - } + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 }, { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } + "clientIndex": 1, + "drainQueue": true }, { - "clientIndex": 0, + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -8984,44 +10585,26 @@ }, "targetId": 2 }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - }, - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } ], - "path": "collection" + "resumeToken": "" } } - ], + } + }, + { + "clientIndex": 0, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -9039,20 +10622,26 @@ } } }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, { "clientIndex": 0, "watchEntity": { "docs": [ { - "key": "collection/c", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "c" + "key": "a" }, - "version": 3000 + "version": 1000 } ], "targets": [ @@ -9060,26 +10649,39 @@ ] } }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, { "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 3000 - }, + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/c", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "c" + "key": "a" }, - "version": 3000 + "version": 1000 } ], "errorCode": 0, @@ -9097,49 +10699,70 @@ }, { "clientIndex": 1, - "drainQueue": true, - "expectedSnapshotEvents": [ + "userUnlisten": [ + 2, { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } + "filters": [ ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/c", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "c" + "key": "a" }, - "version": 3000 + "version": 1000 } ], "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -9149,31 +10772,7 @@ "path": "collection" } } - ] - } - ] - }, - "Query is rejected and re-listened to": { - "describeName": "Listens:", - "itName": "Query is rejected and re-listened to", - "tags": [ - ], - "config": { - "numClients": 1, - "useGarbageCollection": false - }, - "steps": [ - { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, + ], "expectedState": { "activeTargets": { "2": { @@ -9192,44 +10791,8 @@ } }, { - "watchRemove": { - "cause": { - "code": 8 - }, - "targetIds": [ - 2 - ] - }, - "expectedSnapshotEvents": [ - { - "errorCode": 8, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], - "expectedState": { - "activeTargets": { - } - } - }, - { - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, + "clientIndex": 0, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -9242,19 +10805,32 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } } } }, { + "clientIndex": 0, "watchAck": [ 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } ], "targets": [ 2 @@ -9262,21 +10838,40 @@ } }, { + "clientIndex": 0, "watchCurrent": [ [ 2 ], - "resume-token-1000" + "resume-token-2000" ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 - }, + "version": 2000 + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -9292,14 +10887,14 @@ } ] }, - "Query is rejected and re-listened to by secondary client": { + "Query is shared between primary and secondary client": { "describeName": "Listens:", - "itName": "Query is rejected and re-listened to by secondary client", + "itName": "Query is shared between primary and secondary client", "tags": [ "multi-client" ], "config": { - "numClients": 2, + "numClients": 3, "useGarbageCollection": false }, "steps": [ @@ -9314,11 +10909,7 @@ "clientIndex": 0 }, { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 1, + "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -9348,45 +10939,63 @@ }, { "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } - } + "watchAck": [ + 2 + ] }, { "clientIndex": 0, - "watchRemove": { - "cause": { - "code": 8 - }, - "targetIds": [ + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ 2 ] - }, - "expectedState": { - "activeTargets": { - } } }, { - "clientIndex": 1, - "drainQueue": true, + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, "expectedSnapshotEvents": [ { - "errorCode": 8, + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, "fromCache": false, "hasPendingWrites": false, "query": { @@ -9399,6 +11008,10 @@ } ] }, + { + "clientIndex": 1, + "drainQueue": true + }, { "clientIndex": 1, "userListen": { @@ -9411,6 +11024,33 @@ }, "targetId": 2 }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { @@ -9429,8 +11069,48 @@ } }, { - "clientIndex": 0, - "drainQueue": true, + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { @@ -9450,42 +11130,113 @@ }, { "clientIndex": 0, - "watchAck": [ - 2 - ] + "drainQueue": true }, { "clientIndex": 0, "watchEntity": { "docs": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } ], "targets": [ 2 ] } }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, { "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 - } + "version": 2000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] }, { "clientIndex": 1, "drainQueue": true, "expectedSnapshotEvents": [ { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 2000 + } + ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -9501,9 +11252,9 @@ } ] }, - "Query is rejected by primary client": { + "Query is unlistened to by primary client": { "describeName": "Listens:", - "itName": "Query is rejected by primary client", + "itName": "Query is unlistened to by primary client", "tags": [ "multi-client" ], @@ -9523,11 +11274,7 @@ "clientIndex": 0 }, { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 1, + "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -9557,45 +11304,63 @@ }, { "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } - } + "watchAck": [ + 2 + ] }, { "clientIndex": 0, - "watchRemove": { - "cause": { - "code": 8 - }, - "targetIds": [ + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "targets": [ 2 ] - }, - "expectedState": { - "activeTargets": { - } } }, { - "clientIndex": 1, - "drainQueue": true, + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, "expectedSnapshotEvents": [ { - "errorCode": 8, + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, "fromCache": false, "hasPendingWrites": false, "query": { @@ -9607,29 +11372,6 @@ } } ] - } - ] - }, - "Query is resumed by secondary client": { - "describeName": "Listens:", - "itName": "Query is resumed by secondary client", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 2, - "useGarbageCollection": false - }, - "steps": [ - { - "clientIndex": 0, - "drainQueue": true - }, - { - "applyClientState": { - "visibility": "visible" - }, - "clientIndex": 0 }, { "clientIndex": 1, @@ -9647,6 +11389,33 @@ }, "targetId": 2 }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { @@ -9666,7 +11435,20 @@ }, { "clientIndex": 0, - "drainQueue": true, + "drainQueue": true + }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], "expectedState": { "activeTargets": { "2": { @@ -9684,18 +11466,12 @@ } } }, - { - "clientIndex": 0, - "watchAck": [ - 2 - ] - }, { "clientIndex": 0, "watchEntity": { "docs": [ { - "key": "collection/a", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -9703,7 +11479,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "targets": [ @@ -9711,21 +11487,12 @@ ] } }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, { "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 + "version": 2000 } }, { @@ -9735,7 +11502,7 @@ { "added": [ { - "key": "collection/a", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -9743,7 +11510,7 @@ "value": { "key": "a" }, - "version": 1000 + "version": 2000 } ], "errorCode": 0, @@ -9783,21 +11550,40 @@ "activeTargets": { } } - }, + } + ] + }, + "Query only raises events in participating clients": { + "describeName": "Listens:", + "itName": "Query only raises events in participating clients", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 4, + "useGarbageCollection": false + }, + "steps": [ { "clientIndex": 0, - "watchRemove": { - "targetIds": [ - 2 - ] - } + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0 }, { "clientIndex": 1, "drainQueue": true }, { - "clientIndex": 1, + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, "userListen": { "query": { "filters": [ @@ -9808,33 +11594,39 @@ }, "targetId": 2 }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } ], - "path": "collection" + "resumeToken": "" } } - ], + } + }, + { + "clientIndex": 3, + "drainQueue": true + }, + { + "clientIndex": 3, + "userListen": { + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "targetId": 2 + }, "expectedState": { "activeTargets": { "2": { @@ -9867,7 +11659,7 @@ "path": "collection" } ], - "resumeToken": "resume-token-1000" + "resumeToken": "" } } } @@ -9883,7 +11675,7 @@ "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -9891,39 +11683,74 @@ "value": { "key": "a" }, - "version": 2000 + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" } - ], - "targets": [ - 2 - ] - } - }, - { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-2000" + } ] }, { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ - ], - "version": 2000 - } - }, - { - "clientIndex": 1, + "clientIndex": 3, "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -9931,7 +11758,7 @@ "value": { "key": "a" }, - "version": 2000 + "version": 1000 } ], "errorCode": 0, @@ -9949,26 +11776,23 @@ } ] }, - "Query is shared between primary and secondary client": { + "Query recovers after primary takeover": { "describeName": "Listens:", - "itName": "Query is shared between primary and secondary client", + "itName": "Query recovers after primary takeover", "tags": [ "multi-client" ], "config": { - "numClients": 3, + "numClients": 2, "useGarbageCollection": false }, "steps": [ { "clientIndex": 0, - "drainQueue": true - }, - { - "applyClientState": { - "visibility": "visible" - }, - "clientIndex": 0 + "drainQueue": true, + "expectedState": { + "isPrimary": true + } }, { "clientIndex": 0, @@ -10131,48 +11955,10 @@ } }, { - "clientIndex": 2, - "drainQueue": true - }, - { - "clientIndex": 2, - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 + "applyClientState": { + "primary": true }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ], + "clientIndex": 1, "expectedState": { "activeTargets": { "2": { @@ -10185,17 +11971,20 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } - } + }, + "isPrimary": true } }, { - "clientIndex": 0, - "drainQueue": true + "clientIndex": 1, + "watchAck": [ + 2 + ] }, { - "clientIndex": 0, + "clientIndex": 1, "watchEntity": { "docs": [ { @@ -10205,7 +11994,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "b" }, "version": 2000 } @@ -10216,174 +12005,71 @@ } }, { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ + "clientIndex": 1, + "watchCurrent": [ + [ + 2 ], - "version": 2000 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } + "resume-token-2000" ] }, { "clientIndex": 1, - "drainQueue": true, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - }, - { - "clientIndex": 2, - "drainQueue": true, + "watchSnapshot": { + "targetIds": [ + ], + "version": 2000 + }, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - } - ] - }, - "Query is unlistened to by primary client": { - "describeName": "Listens:", - "itName": "Query is unlistened to by primary client", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 2, - "useGarbageCollection": false - }, - "steps": [ - { - "clientIndex": 0, - "drainQueue": true - }, - { - "applyClientState": { - "visibility": "visible" - }, - "clientIndex": 0 - }, - { - "clientIndex": 0, - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "b" + }, + "version": 2000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ ], - "resumeToken": "" + "orderBys": [ + ], + "path": "collection" } } - } + ] }, { "clientIndex": 0, - "watchAck": [ - 2 - ] + "drainQueue": true }, { - "clientIndex": 0, + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "watchEntity": { "docs": [ { - "key": "collection/a", + "key": "collection/c", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "a" + "key": "c" }, - "version": 1000 + "version": 3000 } ], "targets": [ @@ -10392,34 +12078,25 @@ } }, { - "clientIndex": 0, - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, - { - "clientIndex": 0, + "clientIndex": 1, "watchSnapshot": { "targetIds": [ ], - "version": 1000 + "version": 3000 }, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/a", + "key": "collection/c", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "a" + "key": "c" }, - "version": 1000 + "version": 3000 } ], "errorCode": 0, @@ -10436,34 +12113,36 @@ ] }, { - "clientIndex": 1, + "clientIndex": 0, "drainQueue": true }, { - "clientIndex": 1, - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, + "clientIndex": 0, + "runTimer": "client_metadata_refresh", "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/a", + "key": "collection/b", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "a" + "key": "b" }, - "version": 1000 + "version": 2000 + }, + { + "key": "collection/c", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "c" + }, + "version": 3000 } ], "errorCode": 0, @@ -10479,38 +12158,32 @@ } ], "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } + "isPrimary": false } - }, - { - "clientIndex": 0, - "drainQueue": true - }, + } + ] + }, + "Re-opens target without existence filter": { + "describeName": "Listens:", + "itName": "Re-opens target without existence filter", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": false + }, + "steps": [ { - "clientIndex": 0, - "userUnlisten": [ - 2, - { + "userListen": { + "query": { "filters": [ ], "orderBys": [ ], "path": "collection" - } - ], + }, + "targetId": 2 + }, "expectedState": { "activeTargets": { "2": { @@ -10529,11 +12202,15 @@ } }, { - "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -10541,7 +12218,7 @@ "value": { "key": "a" }, - "version": 2000 + "version": 1000 } ], "targets": [ @@ -10550,21 +12227,24 @@ } }, { - "clientIndex": 0, - "watchSnapshot": { - "targetIds": [ + "watchCurrent": [ + [ + 2 ], - "version": 2000 - } + "resume-token-1000" + ] }, { - "clientIndex": 1, - "drainQueue": true, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false @@ -10572,7 +12252,7 @@ "value": { "key": "a" }, - "version": 2000 + "version": 1000 } ], "errorCode": 0, @@ -10589,7 +12269,6 @@ ] }, { - "clientIndex": 1, "userUnlisten": [ 2, { @@ -10606,79 +12285,13 @@ } }, { - "clientIndex": 0, - "drainQueue": true, - "expectedState": { - "activeTargets": { - } - } - } - ] - }, - "Query only raises events in participating clients": { - "describeName": "Listens:", - "itName": "Query only raises events in participating clients", - "tags": [ - "multi-client" - ], - "config": { - "numClients": 4, - "useGarbageCollection": false - }, - "steps": [ - { - "clientIndex": 0, - "drainQueue": true - }, - { - "applyClientState": { - "visibility": "visible" - }, - "clientIndex": 0 - }, - { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 2, - "drainQueue": true - }, - { - "clientIndex": 2, - "userListen": { - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - }, - "targetId": 2 - }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - ], - "resumeToken": "" - } - } + "watchRemove": { + "targetIds": [ + 2 + ] } }, { - "clientIndex": 3, - "drainQueue": true - }, - { - "clientIndex": 3, "userListen": { "query": { "filters": [ @@ -10689,26 +12302,33 @@ }, "targetId": 2 }, - "expectedState": { - "activeTargets": { - "2": { - "queries": [ - { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "a" + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ ], - "resumeToken": "" + "orderBys": [ + ], + "path": "collection" } } - } - }, - { - "clientIndex": 0, - "drainQueue": true, + ], "expectedState": { "activeTargets": { "2": { @@ -10721,77 +12341,46 @@ "path": "collection" } ], - "resumeToken": "" + "resumeToken": "resume-token-1000" } } } }, { - "clientIndex": 0, "watchAck": [ 2 ] }, { - "clientIndex": 0, "watchEntity": { "docs": [ { "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 + "value": null, + "version": 2000 } ], - "targets": [ + "removedTargets": [ 2 ] } }, { - "clientIndex": 0, "watchCurrent": [ [ 2 ], - "resume-token-1000" + "resume-token-2000" ] }, { - "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 1000 - } - }, - { - "clientIndex": 1, - "drainQueue": true - }, - { - "clientIndex": 2, - "drainQueue": true, + "version": 2000 + }, "expectedSnapshotEvents": [ { - "added": [ - { - "key": "collection/a", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "a" - }, - "version": 1000 - } - ], "errorCode": 0, "fromCache": false, "hasPendingWrites": false, @@ -10801,16 +12390,8 @@ "orderBys": [ ], "path": "collection" - } - } - ] - }, - { - "clientIndex": 3, - "drainQueue": true, - "expectedSnapshotEvents": [ - { - "added": [ + }, + "removed": [ { "key": "collection/a", "options": { @@ -10822,25 +12403,15 @@ }, "version": 1000 } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + ] } ] } ] }, - "Query recovers after primary takeover": { + "Secondary client advances query state with global snapshot from primary": { "describeName": "Listens:", - "itName": "Query recovers after primary takeover", + "itName": "Secondary client advances query state with global snapshot from primary", "tags": [ "multi-client" ], @@ -10851,7 +12422,13 @@ "steps": [ { "clientIndex": 0, - "drainQueue": true, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0, "expectedState": { "isPrimary": true } @@ -10902,7 +12479,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } @@ -10938,7 +12515,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } @@ -10956,6 +12533,31 @@ } ] }, + { + "clientIndex": 0, + "userUnlisten": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "clientIndex": 0, + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, { "clientIndex": 1, "drainQueue": true @@ -10982,13 +12584,13 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } ], "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false, "query": { "filters": [ @@ -11017,10 +12619,8 @@ } }, { - "applyClientState": { - "primary": true - }, - "clientIndex": 1, + "clientIndex": 0, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -11035,29 +12635,142 @@ ], "resumeToken": "resume-token-1000" } - }, - "isPrimary": true + } } }, { - "clientIndex": 1, + "clientIndex": 0, "watchAck": [ 2 ] }, { - "clientIndex": 1, + "clientIndex": 0, "watchEntity": { "docs": [ { - "key": "collection/b", + "key": "collection/a", "options": { "hasCommittedMutations": false, "hasLocalMutations": false }, "value": { - "key": "b" + "key": "1" }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "clientIndex": 0, + "watchCurrent": [ + [ + 2 + ], + "resume-token-1500" + ] + }, + { + "clientIndex": 0, + "watchSnapshot": { + "targetIds": [ + ], + "version": 1500 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userDelete": "collection/a" + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "1" + }, + "version": 1000 + } + ] + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "clientIndex": 0, + "watchAck": [ + 2 + ] + }, + { + "clientIndex": 0, + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "value": null, "version": 2000 } ], @@ -11067,7 +12780,7 @@ } }, { - "clientIndex": 1, + "clientIndex": 0, "watchCurrent": [ [ 2 @@ -11076,140 +12789,51 @@ ] }, { - "clientIndex": 1, + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], "version": 2000 - }, - "expectedSnapshotEvents": [ - { - "added": [ - { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } - } - ] - }, - { - "clientIndex": 0, - "drainQueue": true + } }, { "clientIndex": 1, "drainQueue": true }, { - "clientIndex": 1, - "watchEntity": { - "docs": [ - { - "key": "collection/c", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "c" - }, - "version": 3000 - } - ], - "targets": [ - 2 - ] - } + "clientIndex": 0, + "drainQueue": true }, { - "clientIndex": 1, - "watchSnapshot": { - "targetIds": [ - ], - "version": 3000 - }, - "expectedSnapshotEvents": [ + "clientIndex": 0, + "userSet": [ + "collection/a", { - "added": [ - { - "key": "collection/c", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "c" - }, - "version": 3000 - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false, - "query": { - "filters": [ - ], - "orderBys": [ - ], - "path": "collection" - } + "key": "2" } ] }, { - "clientIndex": 0, - "drainQueue": true - }, - { - "clientIndex": 0, - "runTimer": "client_metadata_refresh", + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "added": [ { - "key": "collection/b", - "options": { - "hasCommittedMutations": false, - "hasLocalMutations": false - }, - "value": { - "key": "b" - }, - "version": 2000 - }, - { - "key": "collection/c", + "key": "collection/a", "options": { "hasCommittedMutations": false, - "hasLocalMutations": false + "hasLocalMutations": true }, "value": { - "key": "c" + "key": "2" }, - "version": 3000 + "version": 0 } ], "errorCode": 0, "fromCache": false, - "hasPendingWrites": false, + "hasPendingWrites": true, "query": { "filters": [ ], @@ -11218,24 +12842,36 @@ "path": "collection" } } - ], - "expectedState": { - "isPrimary": false - } + ] } ] }, - "Re-opens target without existence filter": { + "Secondary client raises latency compensated snapshot from primary mutation": { "describeName": "Listens:", - "itName": "Re-opens target without existence filter", + "itName": "Secondary client raises latency compensated snapshot from primary mutation", "tags": [ + "multi-client" ], "config": { - "numClients": 1, + "numClients": 2, "useGarbageCollection": false }, "steps": [ { + "clientIndex": 0, + "drainQueue": true + }, + { + "applyClientState": { + "visibility": "visible" + }, + "clientIndex": 0, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 0, "userListen": { "query": { "filters": [ @@ -11264,11 +12900,13 @@ } }, { + "clientIndex": 0, "watchAck": [ 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ { @@ -11278,7 +12916,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } @@ -11289,6 +12927,7 @@ } }, { + "clientIndex": 0, "watchCurrent": [ [ 2 @@ -11297,6 +12936,7 @@ ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], @@ -11312,7 +12952,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } @@ -11331,6 +12971,7 @@ ] }, { + "clientIndex": 0, "userUnlisten": [ 2, { @@ -11347,6 +12988,7 @@ } }, { + "clientIndex": 0, "watchRemove": { "targetIds": [ 2 @@ -11354,6 +12996,11 @@ } }, { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, "userListen": { "query": { "filters": [ @@ -11374,7 +13021,7 @@ "hasLocalMutations": false }, "value": { - "key": "a" + "key": "1" }, "version": 1000 } @@ -11391,6 +13038,26 @@ } } ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "clientIndex": 0, + "drainQueue": true, "expectedState": { "activeTargets": { "2": { @@ -11409,38 +13076,52 @@ } }, { + "clientIndex": 0, "watchAck": [ 2 ] }, { + "clientIndex": 0, "watchEntity": { "docs": [ { "key": "collection/a", - "value": null, - "version": 2000 + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "key": "1" + }, + "version": 1000 } ], - "removedTargets": [ + "targets": [ 2 ] } }, { + "clientIndex": 0, "watchCurrent": [ [ 2 ], - "resume-token-2000" + "resume-token-1500" ] }, { + "clientIndex": 0, "watchSnapshot": { "targetIds": [ ], - "version": 2000 - }, + "version": 1500 + } + }, + { + "clientIndex": 1, + "drainQueue": true, "expectedSnapshotEvents": [ { "errorCode": 0, @@ -11452,20 +13133,51 @@ "orderBys": [ ], "path": "collection" - }, - "removed": [ + } + } + ] + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userSet": [ + "collection/a", + { + "key": "2" + } + ] + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true, + "modified": [ { "key": "collection/a", "options": { "hasCommittedMutations": false, - "hasLocalMutations": false + "hasLocalMutations": true }, "value": { - "key": "a" + "key": "2" }, - "version": 1000 + "version": 0 } - ] + ], + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } } ] } @@ -14035,7 +15747,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -14251,7 +15963,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -14295,7 +16007,7 @@ "value": { "v": 3 }, - "version": 1000 + "version": 0 } ], "query": { @@ -14334,7 +16046,7 @@ "value": { "v": 4 }, - "version": 1000 + "version": 0 } ], "query": { @@ -14602,7 +16314,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -14627,7 +16339,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { diff --git a/firebase-firestore/src/test/resources/json/orderby_spec_test.json b/firebase-firestore/src/test/resources/json/orderby_spec_test.json index 858919c3721..6c395803606 100644 --- a/firebase-firestore/src/test/resources/json/orderby_spec_test.json +++ b/firebase-firestore/src/test/resources/json/orderby_spec_test.json @@ -154,7 +154,7 @@ "key": "b", "sort": 2 }, - "version": 1001 + "version": 0 } ], "errorCode": 0, diff --git a/firebase-firestore/src/test/resources/json/perf_spec_test.json b/firebase-firestore/src/test/resources/json/perf_spec_test.json index 5276fb92fb9..8dc52457023 100644 --- a/firebase-firestore/src/test/resources/json/perf_spec_test.json +++ b/firebase-firestore/src/test/resources/json/perf_spec_test.json @@ -224390,7 +224390,7 @@ "value": { "v": 1 }, - "version": 2 + "version": 0 } ], "query": { @@ -224493,7 +224493,7 @@ "value": { "v": 2 }, - "version": 4 + "version": 0 } ], "query": { @@ -224596,7 +224596,7 @@ "value": { "v": 3 }, - "version": 6 + "version": 0 } ], "query": { @@ -224699,7 +224699,7 @@ "value": { "v": 4 }, - "version": 8 + "version": 0 } ], "query": { @@ -224802,7 +224802,7 @@ "value": { "v": 5 }, - "version": 10 + "version": 0 } ], "query": { @@ -224905,7 +224905,7 @@ "value": { "v": 6 }, - "version": 12 + "version": 0 } ], "query": { @@ -225008,7 +225008,7 @@ "value": { "v": 7 }, - "version": 14 + "version": 0 } ], "query": { @@ -225111,7 +225111,7 @@ "value": { "v": 8 }, - "version": 16 + "version": 0 } ], "query": { @@ -225214,7 +225214,7 @@ "value": { "v": 9 }, - "version": 18 + "version": 0 } ], "query": { @@ -225317,7 +225317,7 @@ "value": { "v": 10 }, - "version": 20 + "version": 0 } ], "query": { diff --git a/firebase-firestore/src/test/resources/json/query_spec_test.json b/firebase-firestore/src/test/resources/json/query_spec_test.json index f2c1ce51e6c..18ade7a1e2c 100644 --- a/firebase-firestore/src/test/resources/json/query_spec_test.json +++ b/firebase-firestore/src/test/resources/json/query_spec_test.json @@ -1493,7 +1493,7 @@ "value": { "match": true }, - "version": 1000 + "version": 0 } ], "query": { @@ -1534,7 +1534,7 @@ "value": { "match": true }, - "version": 1000 + "version": 0 } ], "errorCode": 0, diff --git a/firebase-firestore/src/test/resources/json/recovery_spec_test.json b/firebase-firestore/src/test/resources/json/recovery_spec_test.json index c42d58c9d41..9ae9f2349ff 100644 --- a/firebase-firestore/src/test/resources/json/recovery_spec_test.json +++ b/firebase-firestore/src/test/resources/json/recovery_spec_test.json @@ -1012,6 +1012,20 @@ }, "targetId": 2 }, + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], "expectedState": { "activeTargets": { "2": { diff --git a/firebase-firestore/src/test/resources/json/write_spec_test.json b/firebase-firestore/src/test/resources/json/write_spec_test.json index 4ad3f39f4be..dc5c135b72f 100644 --- a/firebase-firestore/src/test/resources/json/write_spec_test.json +++ b/firebase-firestore/src/test/resources/json/write_spec_test.json @@ -126,7 +126,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -336,7 +336,7 @@ "value": { "v": 3 }, - "version": 1000 + "version": 0 } ], "query": { @@ -597,7 +597,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -1092,7 +1092,7 @@ }, "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -1308,7 +1308,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -1567,7 +1567,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -1858,7 +1858,7 @@ "local": 5, "remote": 2 }, - "version": 2000 + "version": 0 } ], "query": { @@ -5547,7 +5547,7 @@ "value": { "v": 2 }, - "version": 1000 + "version": 0 } ], "query": { @@ -5650,7 +5650,7 @@ "value": { "v": 2 }, - "version": 500 + "version": 0 } ], "query": { diff --git a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java index e8fac824b72..55f846f9270 100644 --- a/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java +++ b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/testutil/TestUtil.java @@ -254,6 +254,14 @@ public static ImmutableSortedSet keySet(DocumentKey... keys) { return keySet; } + public static Map keyMap(Object... entries) { + Map res = new LinkedHashMap<>(); + for (int i = 0; i < entries.length; i += 2) { + res.put(DocumentKey.fromPathString((String) entries[i]), (T) entries[i + 1]); + } + return res; + } + public static FieldFilter filter(String key, String operator, Object value) { return FieldFilter.create(field(key), operatorFromString(operator), wrap(value)); } diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index 5e3bddd2072..ffd54ae2733 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,4 +1,15 @@ # Unreleased +* [changed] Avoid executing code on the UI thread as much as possible. +* [changed] Internal infrastructure improvements. + +# 20.2.1 +* [changed] Updated dependency of `firebase-iid` to its latest + version (v21.1.0). + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-functions` library. The Kotlin extensions library has no additional +updates. # 20.2.0 * [unchanged] Updated to accommodate the release of the updated diff --git a/firebase-functions/firebase-functions.gradle b/firebase-functions/firebase-functions.gradle deleted file mode 100644 index 00689311183..00000000000 --- a/firebase-functions/firebase-functions.gradle +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' -} - -firebaseLibrary { - testLab.enabled = true - publishSources = true -} - -android { - adbOptions { - timeOutInMs 60 * 1000 - } - - compileSdkVersion project.targetSdkVersion - defaultConfig { - targetSdkVersion project.targetSdkVersion - minSdkVersion 16 - versionName version - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard.txt' - } - sourceSets { - androidTest { - java { - srcDir 'src/testUtil' - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':appcheck:firebase-appcheck-interop') - implementation 'com.google.android.gms:play-services-basement:18.1.0' - implementation 'com.google.android.gms:play-services-base:18.0.1' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation ('com.google.firebase:firebase-iid:20.0.1') { - exclude group: 'com.google.firebase', module: 'firebase-common' - } - implementation ('com.google.firebase:firebase-auth-interop:18.0.0') { - exclude group: 'com.google.firebase', module: 'firebase-common' - } - implementation 'com.google.firebase:firebase-iid-interop:17.0.0' - - implementation 'com.squareup.okhttp3:okhttp:3.12.1' - - annotationProcessor 'com.google.auto.value:auto-value:1.6.2' - - javadocClasspath 'com.google.code.findbugs:jsr305:3.0.2' - javadocClasspath 'org.codehaus.mojo:animal-sniffer-annotations:1.19' - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' - - androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation "com.google.truth:truth:$googleTruthVersion" - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.28.1' - androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.28.1' - - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:rules:1.2.0' - testImplementation 'androidx.test:core:1.2.0' -} - -// ========================================================================== -// Copy from here down if you want to use the google-services plugin in your -// androidTest integration tests. -// ========================================================================== -ext.packageName = "com.google.firebase.functions" -apply from: '../gradle/googleServices.gradle' diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts new file mode 100644 index 00000000000..768690de507 --- /dev/null +++ b/firebase-functions/firebase-functions.gradle.kts @@ -0,0 +1,96 @@ +// Copyright 2022 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("firebase-vendor") +} + +firebaseLibrary { + testLab.enabled = true + publishSources = true +} + +android { + val targetSdkVersion : Int by rootProject + + compileSdk = targetSdkVersion + defaultConfig { + minSdk = 16 + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("proguard.txt") + } + sourceSets { + getByName("androidTest").java.srcDirs("src/testUtil") + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-annotations")) + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) + implementation(project(":appcheck:firebase-appcheck-interop")) + implementation(libs.playservices.base) + implementation(libs.playservices.basement) + implementation(libs.playservices.tasks) + implementation("com.google.firebase:firebase-iid:21.1.0") { + exclude(group = "com.google.firebase", module = "firebase-common") + exclude(group = "com.google.firebase", module = "firebase-components") + } + implementation("com.google.firebase:firebase-auth-interop:18.0.0") { + exclude(group = "com.google.firebase", module = "firebase-common") + } + implementation("com.google.firebase:firebase-iid-interop:17.1.0") + implementation(libs.okhttp) + + implementation(libs.javax.inject) + vendor(libs.dagger.dagger) { + exclude(group = "javax.inject", module = "javax.inject") + } + annotationProcessor(libs.dagger.compiler) + + annotationProcessor(libs.autovalue) + javadocClasspath(libs.findbugs.jsr305) + javadocClasspath("org.codehaus.mojo:animal-sniffer-annotations:1.21") + javadocClasspath(libs.autovalue.annotations) + + testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.robolectric) {} + testImplementation(libs.truth) + testImplementation(libs.androidx.test.rules) + testImplementation(libs.androidx.test.core) + + androidTestImplementation(project(":integ-testing")) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.truth) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.mockito.core) + androidTestImplementation(libs.mockito.dexmaker) +} + +// ========================================================================== +// Copy from here down if you want to use the google-services plugin in your +// androidTest integration tests. +// ========================================================================== +extra["packageName"] = "com.google.firebase.functions" +apply(from = "../gradle/googleServices.gradle") diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index 3ed71215d9c..fbb4efdd3e9 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=20.1.2 -latestReleasedVersion=20.1.1 +version=20.2.2 +latestReleasedVersion=20.2.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-functions/ktx/ktx.gradle b/firebase-functions/ktx/ktx.gradle deleted file mode 100644 index 47c67cc15f3..00000000000 --- a/firebase-functions/ktx/ktx.gradle +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' - id 'kotlin-android' -} - -firebaseLibrary { - releaseWith project(':firebase-functions') - publishJavadoc = true - publishSources = true - testLab.enabled = true -} - -android { - compileSdkVersion project.targetSdkVersion - defaultConfig { - minSdkVersion 16 - multiDexEnabled true - targetSdkVersion project.targetSdkVersion - versionName version - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - test.java { - srcDir 'src/test/kotlin' - } - androidTest.java.srcDirs += 'src/androidTest/kotlin' - } - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - - implementation project(':firebase-common') - implementation project(':firebase-components') - implementation project(':firebase-common:ktx') - implementation project(':firebase-functions') - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - - androidTestImplementation 'junit:junit:4.12' - androidTestImplementation "com.google.truth:truth:$googleTruthVersion" - androidTestImplementation 'androidx.test:runner:1.2.0' - - testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.12' - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'androidx.test:core:1.2.0' -} diff --git a/firebase-functions/ktx/ktx.gradle.kts b/firebase-functions/ktx/ktx.gradle.kts new file mode 100644 index 00000000000..ff451fb3744 --- /dev/null +++ b/firebase-functions/ktx/ktx.gradle.kts @@ -0,0 +1,62 @@ +// Copyright 2022 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("kotlin-android") +} + +firebaseLibrary { + releaseWith(project(":firebase-functions")) + publishJavadoc = true + publishSources = true + testLab.enabled = true +} + +android { + val targetSdkVersion : Int by rootProject + + compileSdk = targetSdkVersion + defaultConfig { + minSdk = 16 + targetSdk = targetSdkVersion + multiDexEnabled = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + sourceSets { + getByName("main").java.srcDirs("src/main/kotlin") + getByName("test").java.srcDirs("src/test/kotlin") + getByName("androidTest").java.srcDirs("src/androidTest/kotlin") + } + testOptions.unitTests.isIncludeAndroidResources = true +} + +dependencies { + implementation(project(":firebase-common")) + implementation(project(":firebase-components")) + implementation(project(":firebase-common:ktx")) + implementation(project(":firebase-functions")) + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.annotation) + implementation(libs.playservices.tasks) + + androidTestImplementation(libs.junit) + androidTestImplementation(libs.truth) + androidTestImplementation(libs.androidx.test.runner) + + testImplementation(libs.robolectric) + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.androidx.test.core) +} diff --git a/firebase-functions/ktx/src/androidTest/kotlin/com/google/firebase/functions/ktx/CallTests.kt b/firebase-functions/ktx/src/androidTest/kotlin/com/google/firebase/functions/ktx/CallTests.kt index 8acae3506bc..7eb3173a57f 100644 --- a/firebase-functions/ktx/src/androidTest/kotlin/com/google/firebase/functions/ktx/CallTests.kt +++ b/firebase-functions/ktx/src/androidTest/kotlin/com/google/firebase/functions/ktx/CallTests.kt @@ -32,56 +32,60 @@ const val API_KEY = "API_KEY" @RunWith(AndroidJUnit4::class) class CallTests { - companion object { - lateinit var app: FirebaseApp + companion object { + lateinit var app: FirebaseApp - @BeforeClass @JvmStatic fun setup() { - app = Firebase.initialize(InstrumentationRegistry.getContext())!! - } + @BeforeClass + @JvmStatic + fun setup() { + app = Firebase.initialize(InstrumentationRegistry.getContext())!! + } - @AfterClass @JvmStatic fun cleanup() { - app.delete() - } + @AfterClass + @JvmStatic + fun cleanup() { + app.delete() } + } - @Test - fun testDataCall() { - val functions = Firebase.functions(app) - val input = hashMapOf( - "bool" to true, - "int" to 2, - "long" to 3L, - "string" to "four", - "array" to listOf(5, 6), - "null" to null - ) + @Test + fun testDataCall() { + val functions = Firebase.functions(app) + val input = + hashMapOf( + "bool" to true, + "int" to 2, + "long" to 3L, + "string" to "four", + "array" to listOf(5, 6), + "null" to null + ) - var function = functions.getHttpsCallable("dataTest") - val actual = Tasks.await(function.call(input)).getData() + var function = functions.getHttpsCallable("dataTest") + val actual = Tasks.await(function.call(input)).getData() - assertThat(actual).isInstanceOf(Map::class.java) - @Suppress("UNCHECKED_CAST") - val map = actual as Map - assertThat(map["message"]).isEqualTo("stub response") - assertThat(map["code"]).isEqualTo(42) - assertThat(map["long"]).isEqualTo(420L) - } + assertThat(actual).isInstanceOf(Map::class.java) + @Suppress("UNCHECKED_CAST") val map = actual as Map + assertThat(map["message"]).isEqualTo("stub response") + assertThat(map["code"]).isEqualTo(42) + assertThat(map["long"]).isEqualTo(420L) + } - @Test - fun testNullDataCall() { - val functions = Firebase.functions(app) - var function = functions.getHttpsCallable("nullTest") - val actual = Tasks.await(function.call(null)).getData() + @Test + fun testNullDataCall() { + val functions = Firebase.functions(app) + var function = functions.getHttpsCallable("nullTest") + val actual = Tasks.await(function.call(null)).getData() - assertThat(actual).isNull() - } + assertThat(actual).isNull() + } - @Test - fun testEmptyDataCall() { - val functions = Firebase.functions(app) - var function = functions.getHttpsCallable("nullTest") - val actual = Tasks.await(function.call()).getData() + @Test + fun testEmptyDataCall() { + val functions = Firebase.functions(app) + var function = functions.getHttpsCallable("nullTest") + val actual = Tasks.await(function.call()).getData() - assertThat(actual).isNull() - } + assertThat(actual).isNull() + } } diff --git a/firebase-functions/ktx/src/main/kotlin/com/google/firebase/functions/ktx/Functions.kt b/firebase-functions/ktx/src/main/kotlin/com/google/firebase/functions/ktx/Functions.kt index 5fc5ad42558..8b06e6c213a 100644 --- a/firebase-functions/ktx/src/main/kotlin/com/google/firebase/functions/ktx/Functions.kt +++ b/firebase-functions/ktx/src/main/kotlin/com/google/firebase/functions/ktx/Functions.kt @@ -24,24 +24,24 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseFunctions] instance of the default [FirebaseApp]. */ val Firebase.functions: FirebaseFunctions - get() = FirebaseFunctions.getInstance() + get() = FirebaseFunctions.getInstance() /** Returns the [FirebaseFunctions] instance of a given [regionOrCustomDomain]. */ fun Firebase.functions(regionOrCustomDomain: String): FirebaseFunctions = - FirebaseFunctions.getInstance(regionOrCustomDomain) + FirebaseFunctions.getInstance(regionOrCustomDomain) /** Returns the [FirebaseFunctions] instance of a given [FirebaseApp]. */ fun Firebase.functions(app: FirebaseApp): FirebaseFunctions = FirebaseFunctions.getInstance(app) /** Returns the [FirebaseFunctions] instance of a given [FirebaseApp] and [regionOrCustomDomain]. */ fun Firebase.functions(app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions = - FirebaseFunctions.getInstance(app, regionOrCustomDomain) + FirebaseFunctions.getInstance(app, regionOrCustomDomain) internal const val LIBRARY_NAME: String = "fire-fun-ktx" /** @suppress */ @Keep class FirebaseFunctionsKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-functions/ktx/src/test/kotlin/com/google/firebase/functions/ktx/FunctionsTests.kt b/firebase-functions/ktx/src/test/kotlin/com/google/firebase/functions/ktx/FunctionsTests.kt index 43e319deb8c..6a5ee934ad0 100644 --- a/firebase-functions/ktx/src/test/kotlin/com/google/firebase/functions/ktx/FunctionsTests.kt +++ b/firebase-functions/ktx/src/test/kotlin/com/google/firebase/functions/ktx/FunctionsTests.kt @@ -35,67 +35,68 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class FunctionsTests : BaseTestCase() { - @Test - fun `functions should delegate to FirebaseFunctions#getInstance()`() { - assertThat(Firebase.functions).isSameInstanceAs(FirebaseFunctions.getInstance()) - } + @Test + fun `functions should delegate to FirebaseFunctions#getInstance()`() { + assertThat(Firebase.functions).isSameInstanceAs(FirebaseFunctions.getInstance()) + } - @Test - fun `FirebaseApp#functions should delegate to FirebaseFunctions#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.functions(app)).isSameInstanceAs(FirebaseFunctions.getInstance(app)) - } + @Test + fun `FirebaseApp#functions should delegate to FirebaseFunctions#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.functions(app)).isSameInstanceAs(FirebaseFunctions.getInstance(app)) + } - @Test - fun `Firebase#functions should delegate to FirebaseFunctions#getInstance(region)`() { - val region = "valid_region" - assertThat(Firebase.functions(region)).isSameInstanceAs(FirebaseFunctions.getInstance(region)) - } + @Test + fun `Firebase#functions should delegate to FirebaseFunctions#getInstance(region)`() { + val region = "valid_region" + assertThat(Firebase.functions(region)).isSameInstanceAs(FirebaseFunctions.getInstance(region)) + } - @Test - fun `Firebase#functions should delegate to FirebaseFunctions#getInstance(FirebaseApp, region)`() { - val app = Firebase.app(EXISTING_APP) - val region = "valid_region" - assertThat(Firebase.functions(app, region)).isSameInstanceAs(FirebaseFunctions.getInstance(app, region)) - } + @Test + fun `Firebase#functions should delegate to FirebaseFunctions#getInstance(FirebaseApp, region)`() { + val app = Firebase.app(EXISTING_APP) + val region = "valid_region" + assertThat(Firebase.functions(app, region)) + .isSameInstanceAs(FirebaseFunctions.getInstance(app, region)) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTest.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTest.java index 73ae20e3464..4029cfdc384 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTest.java +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTest.java @@ -21,10 +21,11 @@ import static org.junit.Assert.assertTrue; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.functions.FirebaseFunctionsException.Code; import java.util.Arrays; import java.util.HashMap; @@ -84,14 +85,15 @@ public void testToken() throws InterruptedException, ExecutionException { // Override the normal token provider to simulate FirebaseAuth being logged in. FirebaseFunctions functions = new FirebaseFunctions( - app, app.getApplicationContext(), app.getOptions().getProjectId(), "us-central1", () -> { HttpsCallableContext context = new HttpsCallableContext("token", null, null); return Tasks.forResult(context); - }); + }, + TestOnlyExecutors.lite(), + TestOnlyExecutors.ui()); HttpsCallableReference function = functions.getHttpsCallable("tokenTest"); Task result = function.call(new HashMap<>()); @@ -105,14 +107,15 @@ public void testInstanceId() throws InterruptedException, ExecutionException { // Override the normal token provider to simulate FirebaseAuth being logged in. FirebaseFunctions functions = new FirebaseFunctions( - app, app.getApplicationContext(), app.getOptions().getProjectId(), "us-central1", () -> { HttpsCallableContext context = new HttpsCallableContext(null, "iid", null); return Tasks.forResult(context); - }); + }, + TestOnlyExecutors.lite(), + TestOnlyExecutors.ui()); HttpsCallableReference function = functions.getHttpsCallable("instanceIdTest"); Task result = function.call(new HashMap<>()); @@ -126,14 +129,15 @@ public void testAppCheck() throws InterruptedException, ExecutionException { // Override the normal token provider to simulate FirebaseAuth being logged in. FirebaseFunctions functions = new FirebaseFunctions( - app, app.getApplicationContext(), app.getOptions().getProjectId(), "us-central1", () -> { HttpsCallableContext context = new HttpsCallableContext(null, null, "appCheck"); return Tasks.forResult(context); - }); + }, + TestOnlyExecutors.lite(), + TestOnlyExecutors.ui()); HttpsCallableReference function = functions.getHttpsCallable("appCheckTest"); Task result = function.call(new HashMap<>()); diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java index 906981d762b..8dd7d93c836 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/FirebaseContextProviderTest.java @@ -20,6 +20,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; import com.google.firebase.auth.internal.InternalAuthProvider; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; import com.google.firebase.inject.Deferred; import com.google.firebase.inject.Provider; @@ -54,7 +55,10 @@ public void getContext_whenAuthAndAppCheckAreNotAvailable_shouldContainOnlyIid() throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( - absentProvider(), providerOf(fixedIidProvider), absentDeferred()); + absentProvider(), + providerOf(fixedIidProvider), + absentDeferred(), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isNull(); @@ -67,7 +71,10 @@ public void getContext_whenOnlyAuthIsAvailable_shouldContainOnlyAuthTokenAndIid( throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( - providerOf(fixedAuthProvider), providerOf(fixedIidProvider), absentDeferred()); + providerOf(fixedAuthProvider), + providerOf(fixedIidProvider), + absentDeferred(), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isEqualTo(AUTH_TOKEN); @@ -80,7 +87,10 @@ public void getContext_whenOnlyAppCheckIsAvailable_shouldContainOnlyAppCheckToke throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( - absentProvider(), providerOf(fixedIidProvider), deferredOf(fixedAppCheckProvider)); + absentProvider(), + providerOf(fixedIidProvider), + deferredOf(fixedAppCheckProvider), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isNull(); @@ -93,7 +103,10 @@ public void getContext_whenOnlyAuthIsAvailableAndNotSignedIn_shouldContainOnlyIi throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( - providerOf(anonymousAuthProvider), providerOf(fixedIidProvider), absentDeferred()); + providerOf(anonymousAuthProvider), + providerOf(fixedIidProvider), + absentDeferred(), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isNull(); @@ -106,7 +119,10 @@ public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyI throws ExecutionException, InterruptedException { FirebaseContextProvider contextProvider = new FirebaseContextProvider( - absentProvider(), providerOf(fixedIidProvider), deferredOf(errorAppCheckProvider)); + absentProvider(), + providerOf(fixedIidProvider), + deferredOf(errorAppCheckProvider), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isNull(); @@ -121,7 +137,8 @@ public void getContext_whenAuthAndAppCheckAreAvailable_shouldContainAuthAppCheck new FirebaseContextProvider( providerOf(fixedAuthProvider), providerOf(fixedIidProvider), - deferredOf(fixedAppCheckProvider)); + deferredOf(fixedAppCheckProvider), + TestOnlyExecutors.lite()); HttpsCallableContext context = Tasks.await(contextProvider.getContext()); assertThat(context.getAuthToken()).isEqualTo(AUTH_TOKEN); diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/TestFirebaseInstanceIdInternal.java b/firebase-functions/src/androidTest/java/com/google/firebase/functions/TestFirebaseInstanceIdInternal.java index 21a68ae5198..05e108c153a 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/TestFirebaseInstanceIdInternal.java +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/TestFirebaseInstanceIdInternal.java @@ -14,8 +14,12 @@ package com.google.firebase.functions; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; +import java.io.IOException; public class TestFirebaseInstanceIdInternal implements FirebaseInstanceIdInternal { private final String testToken; @@ -34,4 +38,20 @@ public String getId() { public String getToken() { return testToken; } + + @NonNull + @Override + public Task getTokenTask() { + return Tasks.forResult(testToken); + } + + @Override + public void deleteToken(@NonNull String s, @NonNull String s1) throws IOException { + // No-op: We're not using this method in our tests + } + + @Override + public void addNewTokenListener(NewTokenListener newTokenListener) { + // No-op: We're not using this method in our tests + } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java index 24a37a1cf8e..a3b85444b24 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java @@ -17,15 +17,20 @@ import android.util.Log; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; +import com.google.firebase.annotations.concurrent.Lightweight; import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; import com.google.firebase.auth.internal.InternalAuthProvider; import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; import com.google.firebase.inject.Deferred; import com.google.firebase.inject.Provider; import com.google.firebase.internal.api.FirebaseNoSignedInUserException; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; +import javax.inject.Inject; +import javax.inject.Singleton; /** A ContextProvider that uses FirebaseAuth to get the token. */ +@Singleton class FirebaseContextProvider implements ContextProvider { private final String TAG = "FirebaseContextProvider"; @@ -33,13 +38,17 @@ class FirebaseContextProvider implements ContextProvider { private final Provider instanceId; private final AtomicReference appCheckRef = new AtomicReference<>(); + private final Executor executor; + @Inject FirebaseContextProvider( Provider tokenProvider, Provider instanceId, - Deferred appCheckDeferred) { + Deferred appCheckDeferred, + @Lightweight Executor executor) { this.tokenProvider = tokenProvider; this.instanceId = instanceId; + this.executor = executor; appCheckDeferred.whenAvailable( p -> { InternalAppCheckTokenProvider appCheck = p.get(); @@ -59,6 +68,7 @@ public Task getContext() { Task appCheckToken = getAppCheckToken(); return Tasks.whenAll(authToken, appCheckToken) .onSuccessTask( + executor, v -> Tasks.forResult( new HttpsCallableContext( @@ -74,6 +84,7 @@ private Task getAuthToken() { } return auth.getAccessToken(false) .continueWith( + executor, task -> { String authToken = null; if (!task.isSuccessful()) { @@ -98,6 +109,7 @@ private Task getAppCheckToken() { return appCheck .getToken(false) .onSuccessTask( + executor, result -> { if (result.getError() != null) { // If there was an error getting the App Check token, do NOT send the placeholder diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java index ffc453bba15..01877019f2d 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java @@ -15,7 +15,6 @@ package com.google.firebase.functions; import android.content.Context; -import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,14 +26,20 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.emulators.EmulatedServiceSettings; import com.google.firebase.functions.FirebaseFunctionsException.Code; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedInject; import java.io.IOException; import java.io.InterruptedIOException; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Executor; +import javax.inject.Named; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; @@ -57,9 +62,6 @@ public class FirebaseFunctions { */ private static boolean providerInstallStarted = false; - // The FirebaseApp instance - private final FirebaseApp app; - // The network client to use for HTTPS requests. private final OkHttpClient client; @@ -69,6 +71,8 @@ public class FirebaseFunctions { // A provider of client metadata to include with calls. private final ContextProvider contextProvider; + private final Executor executor; + // The projectId to use for all functions references. private final String projectId; @@ -84,13 +88,15 @@ public class FirebaseFunctions { // Emulator settings @Nullable private EmulatedServiceSettings emulatorSettings; + @AssistedInject FirebaseFunctions( - FirebaseApp app, Context context, - String projectId, - String regionOrCustomDomain, - ContextProvider contextProvider) { - this.app = app; + @Named("projectId") String projectId, + @Assisted String regionOrCustomDomain, + ContextProvider contextProvider, + @Lightweight Executor executor, + @UiThread Executor uiExecutor) { + this.executor = executor; this.client = new OkHttpClient(); this.serializer = new Serializer(); this.contextProvider = Preconditions.checkNotNull(contextProvider); @@ -112,15 +118,16 @@ public class FirebaseFunctions { this.customDomain = regionOrCustomDomain; } - maybeInstallProviders(context); + maybeInstallProviders(context, uiExecutor); } /** * Runs ProviderInstaller.installIfNeededAsync once per application instance. * * @param context The application context. + * @param uiExecutor */ - private static void maybeInstallProviders(Context context) { + private static void maybeInstallProviders(Context context, Executor uiExecutor) { // Make sure this only runs once. synchronized (providerInstalled) { if (providerInstallStarted) { @@ -131,7 +138,7 @@ private static void maybeInstallProviders(Context context) { // Package installIfNeededAsync into a Runnable so it can be run on the main thread. // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. - Runnable runnable = + uiExecutor.execute( () -> ProviderInstaller.installIfNeededAsync( context, @@ -146,10 +153,7 @@ public void onProviderInstallFailed(int i, android.content.Intent intent) { Log.d("FirebaseFunctions", "Failed to update ssl context"); providerInstalled.setResult(null); } - }); - - Handler handler = new Handler(context.getMainLooper()); - handler.post(runnable); + })); } /** @@ -269,8 +273,9 @@ public void useEmulator(@NonNull String host, int port) { Task call(String name, @Nullable Object data, HttpsCallOptions options) { return providerInstalled .getTask() - .continueWithTask(task -> contextProvider.getContext()) + .continueWithTask(executor, task -> contextProvider.getContext()) .continueWithTask( + executor, task -> { if (!task.isSuccessful()) { return Tasks.forException(task.getException()); @@ -291,8 +296,9 @@ Task call(String name, @Nullable Object data, HttpsCallOpti Task call(URL url, @Nullable Object data, HttpsCallOptions options) { return providerInstalled .getTask() - .continueWithTask(task -> contextProvider.getContext()) + .continueWithTask(executor, task -> contextProvider.getContext()) .continueWithTask( + executor, task -> { if (!task.isSuccessful()) { return Tasks.forException(task.getException()); @@ -305,7 +311,7 @@ Task call(URL url, @Nullable Object data, HttpsCallOptions /** * Calls a Callable HTTPS trigger endpoint. * - * @param name The name of the HTTPS trigger. + * @param url The name of the HTTPS trigger. * @param data Parameters to pass to the function. Can be anything encodable as JSON. * @param context Metadata to supply with the function call. * @return A Task that will be completed when the request is complete. diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java new file mode 100644 index 00000000000..f4e2409701a --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java @@ -0,0 +1,78 @@ +// 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.functions; + +import android.content.Context; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; +import com.google.firebase.auth.internal.InternalAuthProvider; +import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; +import com.google.firebase.inject.Deferred; +import com.google.firebase.inject.Provider; +import dagger.Binds; +import dagger.BindsInstance; +import dagger.Component; +import dagger.Module; +import dagger.Provides; +import java.util.concurrent.Executor; +import javax.inject.Named; +import javax.inject.Singleton; + +/** @hide */ +@Component(modules = FunctionsComponent.MainModule.class) +@Singleton +interface FunctionsComponent { + FunctionsMultiResourceComponent getMultiResourceComponent(); + + @Component.Builder + interface Builder { + @BindsInstance + Builder setApplicationContext(Context applicationContext); + + @BindsInstance + Builder setFirebaseOptions(FirebaseOptions options); + + @BindsInstance + Builder setLiteExecutor(@Lightweight Executor executor); + + @BindsInstance + Builder setUiExecutor(@UiThread Executor executor); + + @BindsInstance + Builder setAuth(Provider auth); + + @BindsInstance + Builder setIid(Provider iid); + + @BindsInstance + Builder setAppCheck(Deferred appCheck); + + FunctionsComponent build(); + } + + @Module + interface MainModule { + @Provides + @Named("projectId") + static String bindProjectId(FirebaseOptions options) { + return options.getProjectId(); + } + + @Binds + ContextProvider contextProvider(FirebaseContextProvider provider); + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java index 6fe3dc8b69d..4a36b27297f 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java @@ -14,13 +14,16 @@ package com.google.firebase.functions; -import android.content.Context; import androidx.annotation.GuardedBy; -import com.google.firebase.FirebaseApp; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; import java.util.HashMap; import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; /** Multi-resource container for Functions. */ +@Singleton class FunctionsMultiResourceComponent { /** * A static map from instance key to FirebaseFunctions instances. Instance keys region names. @@ -30,27 +33,24 @@ class FunctionsMultiResourceComponent { @GuardedBy("this") private final Map instances = new HashMap<>(); - private final Context applicationContext; - private final ContextProvider contextProvider; - private final FirebaseApp app; + private final FirebaseFunctionsFactory functionsFactory; - FunctionsMultiResourceComponent( - Context applicationContext, ContextProvider contextProvider, FirebaseApp app) { - this.applicationContext = applicationContext; - this.contextProvider = contextProvider; - this.app = app; + @Inject + FunctionsMultiResourceComponent(FirebaseFunctionsFactory functionsFactory) { + this.functionsFactory = functionsFactory; } synchronized FirebaseFunctions get(String regionOrCustomDomain) { FirebaseFunctions functions = instances.get(regionOrCustomDomain); - String projectId = app.getOptions().getProjectId(); - if (functions == null) { - functions = - new FirebaseFunctions( - app, applicationContext, projectId, regionOrCustomDomain, contextProvider); + functions = functionsFactory.create(regionOrCustomDomain); instances.put(regionOrCustomDomain, functions); } return functions; } + + @AssistedFactory + interface FirebaseFunctionsFactory { + FirebaseFunctions create(@Assisted String regionOrCustomDomain); + } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java index 9a1033721fb..0f9d1e4c1f7 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java @@ -16,16 +16,20 @@ import android.content.Context; import androidx.annotation.Keep; -import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider; import com.google.firebase.auth.internal.InternalAuthProvider; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * Registers {@link FunctionsMultiResourceComponent}. @@ -38,29 +42,30 @@ public class FunctionsRegistrar implements ComponentRegistrar { @Override public List> getComponents() { + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); return Arrays.asList( - Component.builder(ContextProvider.class) - .add(Dependency.optionalProvider(InternalAuthProvider.class)) - .add(Dependency.requiredProvider(FirebaseInstanceIdInternal.class)) - .add(Dependency.deferred(InternalAppCheckTokenProvider.class)) - .factory( - c -> - new FirebaseContextProvider( - c.getProvider(InternalAuthProvider.class), - c.getProvider(FirebaseInstanceIdInternal.class), - c.getDeferred(InternalAppCheckTokenProvider.class))) - .build(), Component.builder(FunctionsMultiResourceComponent.class) .name(LIBRARY_NAME) .add(Dependency.required(Context.class)) - .add(Dependency.required(ContextProvider.class)) - .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(FirebaseOptions.class)) + .add(Dependency.optionalProvider(InternalAuthProvider.class)) + .add(Dependency.requiredProvider(FirebaseInstanceIdInternal.class)) + .add(Dependency.deferred(InternalAppCheckTokenProvider.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(uiExecutor)) .factory( c -> - new FunctionsMultiResourceComponent( - c.get(Context.class), - c.get(ContextProvider.class), - c.get(FirebaseApp.class))) + DaggerFunctionsComponent.builder() + .setApplicationContext(c.get(Context.class)) + .setFirebaseOptions(c.get(FirebaseOptions.class)) + .setLiteExecutor(c.get(liteExecutor)) + .setUiExecutor(c.get(uiExecutor)) + .setAuth(c.getProvider(InternalAuthProvider.class)) + .setIid(c.getProvider(FirebaseInstanceIdInternal.class)) + .setAppCheck(c.getDeferred(InternalAppCheckTokenProvider.class)) + .build() + .getMultiResourceComponent()) .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java index ec880d92a2d..52201464b58 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java @@ -38,7 +38,7 @@ class Serializer { private final DateFormat dateFormat; - public Serializer() { + Serializer() { // Encode Dates as UTC ISO 8601 strings. dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); diff --git a/firebase-inappmessaging-display/gradle.properties b/firebase-inappmessaging-display/gradle.properties index a23fa35b103..3b49e0fc3a8 100644 --- a/firebase-inappmessaging-display/gradle.properties +++ b/firebase-inappmessaging-display/gradle.properties @@ -1,2 +1,2 @@ -version=20.1.4 -latestReleasedVersion=20.1.3 +version=20.2.1 +latestReleasedVersion=20.2.0 diff --git a/firebase-inappmessaging-display/ktx/src/main/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt b/firebase-inappmessaging-display/ktx/src/main/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt index e5aa56d7642..a0742a8780d 100644 --- a/firebase-inappmessaging-display/ktx/src/main/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt +++ b/firebase-inappmessaging-display/ktx/src/main/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt @@ -24,13 +24,13 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseInAppMessagingDisplay] instance of the default [FirebaseApp]. */ val Firebase.inAppMessagingDisplay: FirebaseInAppMessagingDisplay - get() = FirebaseInAppMessagingDisplay.getInstance() + get() = FirebaseInAppMessagingDisplay.getInstance() internal const val LIBRARY_NAME: String = "fire-iamd-ktx" /** @suppress */ @Keep class FirebaseInAppMessagingDisplayKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-inappmessaging-display/ktx/src/test/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplayTests.kt b/firebase-inappmessaging-display/ktx/src/test/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplayTests.kt index 51469b1fabd..420467d564b 100644 --- a/firebase-inappmessaging-display/ktx/src/test/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplayTests.kt +++ b/firebase-inappmessaging-display/ktx/src/test/kotlin/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplayTests.kt @@ -36,48 +36,49 @@ internal val API_KEY = "ABC" + UUID.randomUUID().toString() const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class InAppMessagingDisplayTests : BaseTestCase() { - @Test - fun `inAppMessagingDisplay should delegate to FirebaseInAppMessagingDisplay#getInstance()`() { - assertThat(Firebase.inAppMessagingDisplay).isSameInstanceAs(FirebaseInAppMessagingDisplay.getInstance()) - } + @Test + fun `inAppMessagingDisplay should delegate to FirebaseInAppMessagingDisplay#getInstance()`() { + assertThat(Firebase.inAppMessagingDisplay) + .isSameInstanceAs(FirebaseInAppMessagingDisplay.getInstance()) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-inappmessaging/CHANGELOG.md b/firebase-inappmessaging/CHANGELOG.md index cae4757c2ee..9a4d46edd93 100644 --- a/firebase-inappmessaging/CHANGELOG.md +++ b/firebase-inappmessaging/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [changed] Migrate firebase-inappmessaging SDK to use common executor pool. # 20.2.0 * [fixed] Fixed a bug that prevented marking more than one message as diff --git a/firebase-inappmessaging/firebase-inappmessaging.gradle b/firebase-inappmessaging/firebase-inappmessaging.gradle index b77b49c78f9..ce3b20a2477 100644 --- a/firebase-inappmessaging/firebase-inappmessaging.gradle +++ b/firebase-inappmessaging/firebase-inappmessaging.gradle @@ -143,6 +143,7 @@ dependencies { testImplementation "io.grpc:grpc-testing:$grpcVersion" testImplementation 'com.google.guava:guava:30.1-android' testImplementation 'androidx.test:core:1.2.0' + testImplementation project(":integ-testing") androidTestImplementation project(':integ-testing') androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/firebase-inappmessaging/gradle.properties b/firebase-inappmessaging/gradle.properties index a23fa35b103..3b49e0fc3a8 100644 --- a/firebase-inappmessaging/gradle.properties +++ b/firebase-inappmessaging/gradle.properties @@ -1,2 +1,2 @@ -version=20.1.4 -latestReleasedVersion=20.1.3 +version=20.2.1 +latestReleasedVersion=20.2.0 diff --git a/firebase-inappmessaging/ktx/src/main/kotlin/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt b/firebase-inappmessaging/ktx/src/main/kotlin/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt index 363be842464..3e866f20e36 100644 --- a/firebase-inappmessaging/ktx/src/main/kotlin/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt +++ b/firebase-inappmessaging/ktx/src/main/kotlin/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt @@ -24,13 +24,13 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseInAppMessaging] instance of the default [FirebaseApp]. */ val Firebase.inAppMessaging: FirebaseInAppMessaging - get() = FirebaseInAppMessaging.getInstance() + get() = FirebaseInAppMessaging.getInstance() internal const val LIBRARY_NAME: String = "fire-iam-ktx" /** @suppress */ @Keep class FirebaseInAppMessagingKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-inappmessaging/ktx/src/test/kotlin/com/google/firebase/inappmessagging/ktx/InAppMessaggingTests.kt b/firebase-inappmessaging/ktx/src/test/kotlin/com/google/firebase/inappmessagging/ktx/InAppMessaggingTests.kt index 73c042cf70f..1eca62157f4 100644 --- a/firebase-inappmessaging/ktx/src/test/kotlin/com/google/firebase/inappmessagging/ktx/InAppMessaggingTests.kt +++ b/firebase-inappmessaging/ktx/src/test/kotlin/com/google/firebase/inappmessagging/ktx/InAppMessaggingTests.kt @@ -36,49 +36,49 @@ internal val API_KEY = "ABC" + UUID.randomUUID().toString() const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setGcmSenderId("ic") - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setGcmSenderId("ic") + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class FirebaseInAppMessagingTests : BaseTestCase() { - @Test - fun `inappmessaging should delegate to FirebaseInAppMessaging#getInstance()`() { - assertThat(Firebase.inAppMessaging).isSameInstanceAs(FirebaseInAppMessaging.getInstance()) - } + @Test + fun `inappmessaging should delegate to FirebaseInAppMessaging#getInstance()`() { + assertThat(Firebase.inAppMessaging).isSameInstanceAs(FirebaseInAppMessaging.getInstance()) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingFlowableTest.java b/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingFlowableTest.java index 0d6ec9e36c2..19f6ef80add 100644 --- a/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingFlowableTest.java +++ b/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingFlowableTest.java @@ -52,6 +52,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.analytics.connector.AnalyticsConnector; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.events.Subscriber; import com.google.firebase.inappmessaging.CommonTypesProto.Event; import com.google.firebase.inappmessaging.CommonTypesProto.Priority; @@ -65,6 +66,7 @@ import com.google.firebase.inappmessaging.internal.TestDeviceHelper; import com.google.firebase.inappmessaging.internal.injection.modules.AppMeasurementModule; import com.google.firebase.inappmessaging.internal.injection.modules.ApplicationModule; +import com.google.firebase.inappmessaging.internal.injection.modules.ExecutorsModule; import com.google.firebase.inappmessaging.internal.injection.modules.GrpcClientModule; import com.google.firebase.inappmessaging.internal.injection.modules.ProgrammaticContextualTriggerFlowableModule; import com.google.firebase.inappmessaging.model.BannerMessage; @@ -270,7 +272,9 @@ public void setUp() { .testSystemClockModule(new TestSystemClockModule(NOW)) .programmaticContextualTriggerFlowableModule( new ProgrammaticContextualTriggerFlowableModule( - new ProgramaticContextualTriggers())); + new ProgramaticContextualTriggers())) + .executorsModule( + new ExecutorsModule(TestOnlyExecutors.background(), TestOnlyExecutors.blocking())); TestUniversalComponent universalComponent = universalComponentBuilder.build(); @@ -315,6 +319,8 @@ public void onAppOpen_whenAnalyticsAbsent_notifiesSubscriber() { TestUniversalComponent analyticsLessUniversalComponent = universalComponentBuilder .appMeasurementModule(new AppMeasurementModule(handler -> {}, firebaseEventSubscriber)) + .executorsModule( + new ExecutorsModule(TestOnlyExecutors.background(), TestOnlyExecutors.blocking())) .build(); TestAppComponent appComponent = appComponentBuilder.universalComponent(analyticsLessUniversalComponent).build(); diff --git a/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/TestUniversalComponent.java b/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/TestUniversalComponent.java index 1bcfa26cd19..92a0b972968 100644 --- a/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/TestUniversalComponent.java +++ b/firebase-inappmessaging/src/androidTest/java/com/google/firebase/inappmessaging/TestUniversalComponent.java @@ -18,6 +18,7 @@ import com.google.firebase.inappmessaging.internal.injection.modules.AnalyticsEventsModule; import com.google.firebase.inappmessaging.internal.injection.modules.AppMeasurementModule; import com.google.firebase.inappmessaging.internal.injection.modules.ApplicationModule; +import com.google.firebase.inappmessaging.internal.injection.modules.ExecutorsModule; import com.google.firebase.inappmessaging.internal.injection.modules.ProgrammaticContextualTriggerFlowableModule; import com.google.firebase.inappmessaging.internal.injection.modules.ProtoStorageClientModule; import com.google.firebase.inappmessaging.internal.injection.modules.RateLimitModule; @@ -41,5 +42,6 @@ ProtoStorageClientModule.class, RateLimitModule.class, AppMeasurementModule.class, + ExecutorsModule.class, }) public interface TestUniversalComponent extends UniversalComponent {} diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessaging.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessaging.java index 48c1c561102..f5db3c274da 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessaging.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessaging.java @@ -14,6 +14,7 @@ package com.google.firebase.inappmessaging; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.common.util.VisibleForTesting; @@ -61,6 +62,8 @@ public class FirebaseInAppMessaging { private boolean areMessagesSuppressed; private FirebaseInAppMessagingDisplay fiamDisplay; + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @VisibleForTesting @Inject FirebaseInAppMessaging( diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingRegistrar.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingRegistrar.java index 2fe096f8fe6..d94c40228e3 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingRegistrar.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingRegistrar.java @@ -22,10 +22,13 @@ import com.google.firebase.abt.FirebaseABTesting; import com.google.firebase.abt.component.AbtComponent; import com.google.firebase.analytics.connector.AnalyticsConnector; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentContainer; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.events.Subscriber; import com.google.firebase.inappmessaging.internal.AbtIntegrationHelper; import com.google.firebase.inappmessaging.internal.ProgramaticContextualTriggers; @@ -37,6 +40,7 @@ import com.google.firebase.inappmessaging.internal.injection.modules.ApiClientModule; import com.google.firebase.inappmessaging.internal.injection.modules.AppMeasurementModule; import com.google.firebase.inappmessaging.internal.injection.modules.ApplicationModule; +import com.google.firebase.inappmessaging.internal.injection.modules.ExecutorsModule; import com.google.firebase.inappmessaging.internal.injection.modules.GrpcClientModule; import com.google.firebase.inappmessaging.internal.injection.modules.ProgrammaticContextualTriggerFlowableModule; import com.google.firebase.inject.Deferred; @@ -44,6 +48,7 @@ import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * Registers {@link FirebaseInAppMessaging}. @@ -53,6 +58,10 @@ @Keep public class FirebaseInAppMessagingRegistrar implements ComponentRegistrar { private static final String LIBRARY_NAME = "fire-fiam"; + private Qualified backgroundExecutor = + Qualified.qualified(Background.class, Executor.class); + private Qualified blockingExecutor = + Qualified.qualified(Blocking.class, Executor.class); @Override @Keep @@ -67,6 +76,8 @@ public List> getComponents() { .add(Dependency.deferred(AnalyticsConnector.class)) .add(Dependency.required(TransportFactory.class)) .add(Dependency.required(Subscriber.class)) + .add(Dependency.required(backgroundExecutor)) + .add(Dependency.required(blockingExecutor)) .factory(this::providesFirebaseInAppMessaging) .eagerInDefaultApp() .build(), @@ -91,6 +102,9 @@ private FirebaseInAppMessaging providesFirebaseInAppMessaging(ComponentContainer .programmaticContextualTriggerFlowableModule( new ProgrammaticContextualTriggerFlowableModule( new ProgramaticContextualTriggers())) + .executorsModule( + new ExecutorsModule( + container.get(backgroundExecutor), container.get(blockingExecutor))) .build(); AppComponent instance = diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/AbtIntegrationHelper.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/AbtIntegrationHelper.java index 8ea864b51f3..988568ed1ad 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/AbtIntegrationHelper.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/AbtIntegrationHelper.java @@ -18,6 +18,7 @@ import com.google.firebase.abt.AbtException; import com.google.firebase.abt.AbtExperimentInfo; import com.google.firebase.abt.FirebaseABTesting; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.inappmessaging.ExperimentPayloadProto; import com.google.firebase.inappmessaging.internal.injection.scopes.FirebaseAppScope; import com.google.internal.firebase.inappmessaging.v1.CampaignProto; @@ -25,7 +26,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import javax.inject.Inject; /** @hide */ @@ -33,7 +33,7 @@ public class AbtIntegrationHelper { private final FirebaseABTesting abTesting; - @VisibleForTesting Executor executor = Executors.newSingleThreadExecutor(); + @Inject @Blocking @VisibleForTesting Executor executor; @Inject public AbtIntegrationHelper(FirebaseABTesting abTesting) { diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManager.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManager.java index ff1d3383849..0e122fe981b 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManager.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManager.java @@ -14,7 +14,7 @@ package com.google.firebase.inappmessaging.internal; -import androidx.annotation.NonNull; +import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.inappmessaging.FirebaseInAppMessagingClickListener; import com.google.firebase.inappmessaging.FirebaseInAppMessagingDismissListener; import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayCallbacks; @@ -24,13 +24,7 @@ import com.google.firebase.inappmessaging.model.InAppMessage; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; /** * A class used to manage and schedule events to registered (ie: developer-defined) or expensive @@ -41,11 +35,7 @@ @SuppressWarnings("JavaDoc") public class DeveloperListenerManager { - // We limit to 1 so there is minimial impact to device performance - private static final int POOL_SIZE = 1; - // Keep alive to minimize chance of having to restart a thread to handle both impression and click - private static final int KEEP_ALIVE_TIME_SECONDS = 15; - public static DeveloperListenerManager instance = new DeveloperListenerManager(); + private final Executor backgroundExecutor; private Map registeredClickListeners = new HashMap<>(); private Map @@ -55,25 +45,15 @@ public class DeveloperListenerManager { private Map registeredImpressionListeners = new HashMap<>(); - private static BlockingQueue mCallbackQueue = new LinkedBlockingQueue<>(); - private static final ThreadPoolExecutor CALLBACK_QUEUE_EXECUTOR = - new ThreadPoolExecutor( - POOL_SIZE, - POOL_SIZE, - KEEP_ALIVE_TIME_SECONDS, - TimeUnit.SECONDS, - mCallbackQueue, - new FIAMThreadFactory("EventListeners-")); - - static { - CALLBACK_QUEUE_EXECUTOR.allowCoreThreadTimeOut(true); + public DeveloperListenerManager(@Background Executor backgroundExecutor) { + this.backgroundExecutor = backgroundExecutor; } // Used internally by MetricsLoggerClient public void impressionDetected(InAppMessage inAppMessage) { for (ImpressionExecutorAndListener listener : registeredImpressionListeners.values()) { listener - .withExecutor(CALLBACK_QUEUE_EXECUTOR) + .withExecutor(backgroundExecutor) .execute(() -> listener.getListener().impressionDetected(inAppMessage)); } } @@ -83,7 +63,7 @@ public void displayErrorEncountered( FirebaseInAppMessagingDisplayCallbacks.InAppMessagingErrorReason errorReason) { for (ErrorsExecutorAndListener listener : registeredErrorListeners.values()) { listener - .withExecutor(CALLBACK_QUEUE_EXECUTOR) + .withExecutor(backgroundExecutor) .execute(() -> listener.getListener().displayErrorEncountered(inAppMessage, errorReason)); } } @@ -91,7 +71,7 @@ public void displayErrorEncountered( public void messageClicked(InAppMessage inAppMessage, Action action) { for (ClicksExecutorAndListener listener : registeredClickListeners.values()) { listener - .withExecutor(CALLBACK_QUEUE_EXECUTOR) + .withExecutor(backgroundExecutor) .execute(() -> listener.getListener().messageClicked(inAppMessage, action)); } } @@ -99,7 +79,7 @@ public void messageClicked(InAppMessage inAppMessage, Action action) { public void messageDismissed(InAppMessage inAppMessage) { for (DismissExecutorAndListener listener : registeredDismissListeners.values()) { listener - .withExecutor(CALLBACK_QUEUE_EXECUTOR) + .withExecutor(backgroundExecutor) .execute(() -> listener.getListener().messageDismissed(inAppMessage)); } } @@ -171,27 +151,6 @@ public void removeAllListeners() { registeredErrorListeners.clear(); } - /** The thread factory for Storage threads. */ - static class FIAMThreadFactory implements ThreadFactory { - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String mNameSuffix; - - FIAMThreadFactory(@NonNull String suffix) { - mNameSuffix = suffix; - } - - @SuppressWarnings("ThreadPriorityCheck") - @Override - public Thread newThread(@NonNull Runnable r) { - Thread t = new Thread(r, "FIAM-" + mNameSuffix + threadNumber.getAndIncrement()); - t.setDaemon(false); - t.setPriority( - android.os.Process.THREAD_PRIORITY_BACKGROUND - + android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE); - return t; - } - } - private abstract static class ExecutorAndListener { private final Executor executor; diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/InAppMessageStreamManager.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/InAppMessageStreamManager.java index bacf96fffad..9d2ab1774dd 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/InAppMessageStreamManager.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/InAppMessageStreamManager.java @@ -14,6 +14,7 @@ package com.google.firebase.inappmessaging.internal; +import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; @@ -387,6 +388,8 @@ static FetchEligibleCampaignsResponse cacheExpiringResponse() { return FetchEligibleCampaignsResponse.newBuilder().setExpirationEpochTimestampMillis(1).build(); } + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") private static Maybe taskToMaybe(Task task) { return Maybe.create( emitter -> { diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/MetricsLoggerClient.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/MetricsLoggerClient.java index 29b0645b497..7d0c0b03330 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/MetricsLoggerClient.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/MetricsLoggerClient.java @@ -17,6 +17,7 @@ import static com.google.firebase.inappmessaging.EventType.CLICK_EVENT_TYPE; import static com.google.firebase.inappmessaging.EventType.IMPRESSION_EVENT_TYPE; +import android.annotation.SuppressLint; import android.os.Bundle; import com.google.firebase.FirebaseApp; import com.google.firebase.analytics.connector.AnalyticsConnector; @@ -95,6 +96,8 @@ public MetricsLoggerClient( } /** Log impression */ + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") void logImpression(InAppMessage message) { if (!isTestCampaign(message)) { // If message is not a test message then log @@ -115,6 +118,8 @@ void logImpression(InAppMessage message) { } /** Log click */ + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") void logMessageClick(InAppMessage message, Action action) { if (!isTestCampaign(message)) { // If message is not a test message then log @@ -132,6 +137,8 @@ void logMessageClick(InAppMessage message, Action action) { } /** Log Rendering error */ + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") void logRenderError(InAppMessage message, InAppMessagingErrorReason errorReason) { if (!isTestCampaign(message)) { // If message is not a test message then log campaign metrics @@ -148,6 +155,8 @@ void logRenderError(InAppMessage message, InAppMessagingErrorReason errorReason) } /** Log dismiss */ + // TODO(b/261014173): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") void logDismiss(InAppMessage message, InAppMessagingDismissType dismissType) { if (!isTestCampaign(message)) { // If message is not a test message then log campaign metrics diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/components/UniversalComponent.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/components/UniversalComponent.java index c4f7f67cc16..1e5842a1ddf 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/components/UniversalComponent.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/components/UniversalComponent.java @@ -28,6 +28,7 @@ import com.google.firebase.inappmessaging.internal.injection.modules.AnalyticsEventsModule; import com.google.firebase.inappmessaging.internal.injection.modules.AppMeasurementModule; import com.google.firebase.inappmessaging.internal.injection.modules.ApplicationModule; +import com.google.firebase.inappmessaging.internal.injection.modules.ExecutorsModule; import com.google.firebase.inappmessaging.internal.injection.modules.ForegroundFlowableModule; import com.google.firebase.inappmessaging.internal.injection.modules.GrpcChannelModule; import com.google.firebase.inappmessaging.internal.injection.modules.ProgrammaticContextualTriggerFlowableModule; @@ -64,7 +65,8 @@ ProtoStorageClientModule.class, SystemClockModule.class, RateLimitModule.class, - AppMeasurementModule.class + AppMeasurementModule.class, + ExecutorsModule.class }) public interface UniversalComponent { ProviderInstaller probiderInstaller(); diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ApplicationModule.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ApplicationModule.java index 96d8b63e1cc..ec4c86ff337 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ApplicationModule.java +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ApplicationModule.java @@ -15,9 +15,11 @@ package com.google.firebase.inappmessaging.internal.injection.modules; import android.app.Application; +import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.inappmessaging.internal.DeveloperListenerManager; import dagger.Module; import dagger.Provides; +import java.util.concurrent.Executor; import javax.inject.Singleton; /** @@ -41,7 +43,8 @@ public Application providesApplication() { @Provides @Singleton - public DeveloperListenerManager developerListenerManager() { - return new DeveloperListenerManager(); + public DeveloperListenerManager developerListenerManager( + @Background Executor backgroundExecutor) { + return new DeveloperListenerManager(backgroundExecutor); } } diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ExecutorsModule.java b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ExecutorsModule.java new file mode 100644 index 00000000000..a8ae32abf04 --- /dev/null +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/injection/modules/ExecutorsModule.java @@ -0,0 +1,53 @@ +// 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.inappmessaging.internal.injection.modules; + +import androidx.annotation.NonNull; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import dagger.Module; +import dagger.Provides; +import java.util.concurrent.Executor; +import javax.inject.Singleton; + +/** Provides executors for running tasks. */ +@Module +public class ExecutorsModule { + private final Executor backgroundExecutor; + private final Executor blockingExecutor; + + public ExecutorsModule( + @NonNull @Background Executor backgroundExecutor, + @NonNull @Blocking Executor blockingExecutor) { + this.backgroundExecutor = backgroundExecutor; + this.blockingExecutor = blockingExecutor; + } + + @Provides + @Singleton + @Background + @NonNull + public Executor providesBackgroundExecutor() { + return backgroundExecutor; + } + + @Provides + @Singleton + @Blocking + @NonNull + public Executor providesBlockingExecutor() { + return blockingExecutor; + } +} diff --git a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingTest.java b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingTest.java index 41e181dd8c3..c5c945c6439 100644 --- a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingTest.java +++ b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/FirebaseInAppMessagingTest.java @@ -33,6 +33,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.inappmessaging.CommonTypesProto.Event; import com.google.firebase.inappmessaging.CommonTypesProto.Priority; import com.google.firebase.inappmessaging.CommonTypesProto.TriggeringCondition; @@ -149,7 +150,11 @@ public Builder toBuilder() { @Mock private DisplayCallbacksFactory displayCallbacksFactory; @Mock private FirebaseInAppMessagingDisplayCallbacks displayCallbacks; @Mock private ProgramaticContextualTriggers programaticContextualTriggers; - @Mock DeveloperListenerManager developerListenerManager = new DeveloperListenerManager(); + + @Mock + DeveloperListenerManager developerListenerManager = + new DeveloperListenerManager(TestOnlyExecutors.background()); + FirebaseApp firebaseApp1; FirebaseOptions options; diff --git a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManagerTest.java b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManagerTest.java index 2bf8517f558..a8ae54c9b6d 100644 --- a/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManagerTest.java +++ b/firebase-inappmessaging/src/test/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManagerTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.inappmessaging.FirebaseInAppMessagingClickListener; import com.google.firebase.inappmessaging.FirebaseInAppMessagingDismissListener; import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayCallbacks; @@ -51,7 +52,7 @@ public class DeveloperListenerManagerTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - developerListenerManager = new DeveloperListenerManager(); + developerListenerManager = new DeveloperListenerManager(TestOnlyExecutors.background()); } @Test diff --git a/firebase-installations-interop/gradle.properties b/firebase-installations-interop/gradle.properties index b58bf86ff76..5328ce212de 100644 --- a/firebase-installations-interop/gradle.properties +++ b/firebase-installations-interop/gradle.properties @@ -1,2 +1,2 @@ -version=17.0.3 -latestReleasedVersion=17.0.2 +version=17.1.1 +latestReleasedVersion=17.1.0 diff --git a/firebase-installations/gradle.properties b/firebase-installations/gradle.properties index 22bb5703b26..5328ce212de 100644 --- a/firebase-installations/gradle.properties +++ b/firebase-installations/gradle.properties @@ -1,2 +1,2 @@ -version=17.0.4 -latestReleasedVersion=17.0.3 +version=17.1.1 +latestReleasedVersion=17.1.0 diff --git a/firebase-installations/ktx/src/main/kotlin/com/google/firebase/installations/ktx/Installations.kt b/firebase-installations/ktx/src/main/kotlin/com/google/firebase/installations/ktx/Installations.kt index 3cc8897a5d7..9ddd79e4b84 100644 --- a/firebase-installations/ktx/src/main/kotlin/com/google/firebase/installations/ktx/Installations.kt +++ b/firebase-installations/ktx/src/main/kotlin/com/google/firebase/installations/ktx/Installations.kt @@ -23,16 +23,16 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebaseInstallations] instance of the default [FirebaseApp]. */ val Firebase.installations: FirebaseInstallations - get() = FirebaseInstallations.getInstance() + get() = FirebaseInstallations.getInstance() /** Returns the [FirebaseInstallations] instance of a given [FirebaseApp]. */ fun Firebase.installations(app: FirebaseApp): FirebaseInstallations = - FirebaseInstallations.getInstance(app) + FirebaseInstallations.getInstance(app) internal const val LIBRARY_NAME: String = "fire-installations-ktx" /** @suppress */ class FirebaseInstallationsKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-installations/ktx/src/test/kotlin/com/google/firebase/installations/ktx/InstallationsTests.kt b/firebase-installations/ktx/src/test/kotlin/com/google/firebase/installations/ktx/InstallationsTests.kt index 367d838e87a..bce5d31ca40 100644 --- a/firebase-installations/ktx/src/test/kotlin/com/google/firebase/installations/ktx/InstallationsTests.kt +++ b/firebase-installations/ktx/src/test/kotlin/com/google/firebase/installations/ktx/InstallationsTests.kt @@ -35,53 +35,54 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class InstallationsTests : BaseTestCase() { - @Test - fun `installations should delegate to FirebaseInstallations#getInstance()`() { - Truth.assertThat(Firebase.installations).isSameInstanceAs(FirebaseInstallations.getInstance()) - } + @Test + fun `installations should delegate to FirebaseInstallations#getInstance()`() { + Truth.assertThat(Firebase.installations).isSameInstanceAs(FirebaseInstallations.getInstance()) + } - @Test - fun `installations(app) should delegate to FirebaseInstallations#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - Truth.assertThat(Firebase.installations(app)).isSameInstanceAs(FirebaseInstallations.getInstance(app)) - } + @Test + fun `installations(app) should delegate to FirebaseInstallations#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + Truth.assertThat(Firebase.installations(app)) + .isSameInstanceAs(FirebaseInstallations.getInstance(app)) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - Truth.assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + Truth.assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index 5f1b1e7f90d..62385d5caca 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -14,6 +14,7 @@ package com.google.firebase.installations; +import android.annotation.SuppressLint; import android.text.TextUtils; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -96,6 +97,8 @@ public class FirebaseInstallations implements FirebaseInstallationsApi { private final AtomicInteger mCount = new AtomicInteger(1); @Override + // TODO(b/258422917): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public Thread newThread(Runnable r) { return new Thread( r, String.format("firebase-installations-executor-%d", mCount.getAndIncrement())); @@ -123,6 +126,8 @@ public Thread newThread(Runnable r) { + "Please retry your last request."; /** package private constructor. */ + // TODO(b/258422917): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") FirebaseInstallations( FirebaseApp firebaseApp, @NonNull Provider heartBeatProvider) { this( @@ -142,6 +147,8 @@ public Thread newThread(Runnable r) { new RandomFidGenerator()); } + // TODO(b/258422917): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") FirebaseInstallations( ExecutorService backgroundExecutor, FirebaseApp firebaseApp, diff --git a/firebase-messaging-directboot/CHANGELOG.md b/firebase-messaging-directboot/CHANGELOG.md index 459f7ec15b7..7ab2c5f2753 100644 --- a/firebase-messaging-directboot/CHANGELOG.md +++ b/firebase-messaging-directboot/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +# 23.1.1 +* [changed] Removed unused classes. + # 23.1.0 * [changed] Internal changes to ensure functionality alignment with other SDK releases. diff --git a/firebase-messaging-directboot/firebase-messaging-directboot.gradle b/firebase-messaging-directboot/firebase-messaging-directboot.gradle index adca39b77ad..aae84d0dc49 100644 --- a/firebase-messaging-directboot/firebase-messaging-directboot.gradle +++ b/firebase-messaging-directboot/firebase-messaging-directboot.gradle @@ -27,10 +27,6 @@ android { timeOutInMs 60 * 1000 } - lintOptions { - abortOnError false - } - compileSdkVersion project.targetSdkVersion defaultConfig { minSdkVersion 19 diff --git a/firebase-messaging-directboot/gradle.properties b/firebase-messaging-directboot/gradle.properties index f46c5c34c0c..77db708897c 100644 --- a/firebase-messaging-directboot/gradle.properties +++ b/firebase-messaging-directboot/gradle.properties @@ -1,3 +1,3 @@ -version=23.0.9 -latestReleasedVersion=23.0.8 +version=23.1.1 +latestReleasedVersion=23.1.0 android.enableUnitTestBinaryResources=true \ No newline at end of file diff --git a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ExecutorFactory.java b/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ExecutorFactory.java deleted file mode 100644 index 6e801ce3227..00000000000 --- a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ExecutorFactory.java +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2020 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.messaging.directboot.threads; - -import com.google.errorprone.annotations.CompileTimeConstant; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; - -/** - * Factory to work as a drop-in replacement for {@link java.util.concurrent.Executors}. - * - *
    - *
  • Specifying a ThreadFactory is not allowed, since thread creation is not at the discretion - * of the client. - *
  • The core pool size is decided globally, and is not at the discretion of any individual - * client. - *
  • Setting the current thread priority or name will affect future tasks and is banned. - *
- * - *

One divergence from Executors is that unused threads are allowed to time out after a period of - * inactivity, so subsequent semi-cold starts may experience a tiny bit of latency. It is not - * expected to be a significant performance concern, but will save significant memory. - */ -public interface ExecutorFactory { - - /** - * Creates a thread pool that creates new threads as needed, but will reuse previously constructed - * threads when they are available. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newCachedThreadPool()}. - * - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newThreadPool(ThreadPriority priority); - - /** - * Creates a thread pool that creates new threads as needed, but will reuse previously constructed - * threads when they are available. - * - *

Drop-in replacement for {@link - * java.util.concurrent.Executors#newCachedThreadPool(ThreadFactory)}. - * - * @param threadFactory the factory to use when creating new threads. - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newThreadPool(ThreadFactory threadFactory, ThreadPriority priority); - - /** - * Creates a thread pool that creates new threads as needed, but will reuse previously constructed - * threads when they are available. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newFixedThreadPool(int)}. - * - * @param maxConcurrency at most this number of tasks will be executed concurrently. Overflow - * tasks will be placed into an unbounded queue. - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newThreadPool(int maxConcurrency, ThreadPriority priority); - - /** - * Creates a thread pool that creates new threads as needed, but will reuse previously constructed - * threads when they are available. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newFixedThreadPool(int, - * ThreadFactory)}. - * - * @param maxConcurrency at most this number of tasks will be executed concurrently. Overflow - * tasks will be placed into an unbounded queue. - * @param threadFactory the factory to use when creating new threads. - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newThreadPool( - int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority); - - /** - * Creates an executor that mimics a single-threaded executor, in the sense that operations can - * happen at most one-at-a-time. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newSingleThreadExecutor()}. - * - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newSingleThreadExecutor(ThreadPriority priority); - - /** - * Creates an executor that mimics a single-threaded executor, in the sense that operations can - * happen at most one-at-a-time. - * - *

Drop-in replacement for {@link - * java.util.concurrent.Executors#newSingleThreadExecutor(ThreadFactory)}. - * - * @param threadFactory the factory to use when creating new threads. - * @param priority see {@link ThreadPriority}. - */ - public ExecutorService newSingleThreadExecutor( - ThreadFactory threadFactory, ThreadPriority priority); - - /** - * Creates a ScheduledThreadPool that allows executing tasks in the future. If you don't require - * this functionality, just use {@link #newCachedThreadPool(int)}. - * - *

WARNING: Do not leak these, since these never terminate their threads. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newScheduledThreadPool(int)}. - * - * @param maxConcurrency at most this number of tasks will be executed concurrently. Overflow - * tasks will be placed into an unbounded queue. - * @param priority see {@link ThreadPriority}. - */ - public ScheduledExecutorService newScheduledThreadPool( - int maxConcurrency, ThreadPriority priority); - - /** - * Creates a ScheduledThreadPool that allows executing tasks in the future. If you don't require - * this functionality, just use {@link #newCachedThreadPool(int)}. - * - *

WARNING: Do not leak these, since these never terminate their threads. - * - *

Drop-in replacement for {@link java.util.concurrent.Executors#newScheduledThreadPool(int, - * ThreadFactory)}. - * - * @param maxConcurrency at most this number of tasks will be executed concurrently. Overflow - * tasks will be placed into an unbounded queue. - * @param threadFactory the factory to use when creating new threads. - * @param priority see {@link ThreadPriority}. - */ - public ScheduledExecutorService newScheduledThreadPool( - int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority); - - /** - * Executes a one-off task where previously {@code new Thread()} may have been used. - * - *

Note that this may use a global, unlimited thread pool. Thus, threads should not generally - * try to manipulate the thread priority or name within the Runnable. - * - * @param moduleName name of the module - * @param name name of the thread; only sometimes honored - * @param priority see {@link ThreadPriority}. - * @param runnable the Runnable to run - */ - public void executeOneOff( - @CompileTimeConstant String moduleName, - @CompileTimeConstant String name, - ThreadPriority priority, - Runnable runnable); - - /** - * Executes a one-off task where previously {@code new Thread()} may have been used. - * - *

This returns a Future to allow you to query the state; you can use {@code Future.isDone()} - * instead of {@code !Thread.isAlive())}, and {@code Future.get()} instead of {@code - * Thread.join()}. - * - * @param moduleName name of the module - * @param name name of the thread; only sometimes honored - * @param priority see {@link ThreadPriority}. - * @param runnable the Runnable to run - */ - public Future submitOneOff( - @CompileTimeConstant String moduleName, - @CompileTimeConstant String name, - ThreadPriority priority, - Runnable runnable); -} diff --git a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/PoolableExecutors.java b/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/PoolableExecutors.java deleted file mode 100644 index 529fe04dcd5..00000000000 --- a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/PoolableExecutors.java +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2020 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.messaging.directboot.threads; - -import androidx.annotation.NonNull; -import com.google.errorprone.annotations.CompileTimeConstant; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Provider of the {@link ExecutorFactory} that should be used to create thread executors. - * - *

If a factory is not provided during initialization, then it provides a default implementation. - */ -// TODO(b/144941134): Remove nullness suppression. -@SuppressWarnings("nullness") -public class PoolableExecutors { - - private static final ExecutorFactory DEFAULT_INSTANCE = new DefaultExecutorFactory(); - private static volatile ExecutorFactory instance = DEFAULT_INSTANCE; - - private PoolableExecutors() {} - - public static ExecutorFactory factory() { - return instance; - } - - /** A {@link ExecutorFactory} that creates the default thread executors. */ - private static class DefaultExecutorFactory implements ExecutorFactory { - - private static final long CORE_THREAD_TIMEOUT_SECS = 60L; - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newThreadPool(ThreadPriority priority) { - // NOTE: Cached threadpools automatically time out all threads. They have no concept of core - // threads; the queue blocks until a thread is started. - return Executors.unconfigurableExecutorService(Executors.newCachedThreadPool()); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newThreadPool(ThreadFactory threadFactory, ThreadPriority priority) { - return Executors.unconfigurableExecutorService(Executors.newCachedThreadPool(threadFactory)); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newThreadPool(int maxConcurrency, ThreadPriority priority) { - // TODO(gboyer): Honor the thread priority even when no factory is provided, by creating - // a factory that actually sets priority. - // TODO(gboyer): Add a @CompileTimeConstant String name argument; this will replace almost - // all uses of the ThreadFactory version, and could later help with tracing. - return newThreadPool(maxConcurrency, Executors.defaultThreadFactory(), priority); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newThreadPool( - int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority) { - ThreadPoolExecutor executor = - new ThreadPoolExecutor( - maxConcurrency, - maxConcurrency, - CORE_THREAD_TIMEOUT_SECS, - TimeUnit.SECONDS, - new LinkedBlockingQueue(), - threadFactory); - // This allows core threads to be terminated if they are not used; otherwise, these threads - // will forever suck up memory until the executor is shutdown or finalized. Generally, it is - // fairly fast in Android to start and stop threads, especially if this is limited to only - // happening once per minute. - executor.allowCoreThreadTimeOut(true); - return Executors.unconfigurableExecutorService(executor); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newSingleThreadExecutor(ThreadPriority priority) { - return newThreadPool(1, priority); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ExecutorService newSingleThreadExecutor( - ThreadFactory threadFactory, ThreadPriority priority) { - return newThreadPool(1, threadFactory, priority); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ScheduledExecutorService newScheduledThreadPool( - int maxConcurrency, ThreadPriority priority) { - // NOTE: There's no way to make a scheduled executor stop threads automatically, because - // at least one thread is needed to waiting for future tasks. - // TODO(b/63802200): Consider wrapping this in a finalizable decorator that prevents runaway - // memory leaks from non-shutdown pools. - return Executors.unconfigurableScheduledExecutorService( - Executors.newScheduledThreadPool(maxConcurrency)); - } - - /** {@inheritDoc} */ - @NonNull - @Override - public ScheduledExecutorService newScheduledThreadPool( - int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority) { - return Executors.unconfigurableScheduledExecutorService( - Executors.newScheduledThreadPool(maxConcurrency, threadFactory)); - } - - @Override - @NonNull - public void executeOneOff( - @CompileTimeConstant final String moduleName, - @CompileTimeConstant final String name, - ThreadPriority priority, - Runnable runnable) { - new Thread(runnable, name).start(); - } - - @Override - @NonNull - public Future submitOneOff( - @CompileTimeConstant final String moduleName, - @CompileTimeConstant final String name, - ThreadPriority priority, - Runnable runnable) { - FutureTask task = new FutureTask<>(runnable, null); - new Thread(task, name).start(); - return task; - } - } - - /** - * Installs the {@link ExecutorFactory} implementation; INTERNAL USE ONLY. - * - *

May only be called once. - * - *

Call this only via build-visibility-restricted PunchClockThreadsImplementationApi. - */ - static void installExecutorFactory(ExecutorFactory instance) { - if (PoolableExecutors.instance != DEFAULT_INSTANCE) { - throw new IllegalStateException("Trying to install an ExecutorFactory twice!"); - } - PoolableExecutors.instance = instance; - } -} diff --git a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ThreadPriority.java b/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ThreadPriority.java deleted file mode 100644 index dcb8eae39b6..00000000000 --- a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/ThreadPriority.java +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 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.messaging.directboot.threads; - -/** - * Commonly used thread priorities. - * - *

A true, but simple, enum is used for type safety to prevent mistakes like thread pool - * factories frequently take in integers that could be mistaken for an {@code IntDef} if the caller - * is not using Android Lint. The {@code @SimpleEnum} annotation is not used because of complex - * downstream usage, so minification by ProGuard depends on the app's usage. - */ -public enum ThreadPriority { - - /** - * Reduced performance but at greater power efficiency. If the user isn't staring at a spinner - * while you're doing your work, or if more CPU power won't help, or if you're just not sure what - * to choose - then this is for you! - * - *

Example use cases: - Doing auto-backup. - Cleaning up a database. - Computing contextual - * signals in the background. - Pretty much anything that's I/O bound (tasks that are mostly disk - * and network access). - A sane default. - * - *

Such tasks will be yield to more important work, but won't be starved, and will still be - * allowed to make forward progress. On some devices with big.LITTLE CPU architectures running - * Marshmallow, your tasks may be locked to the little cores, executing slower but drawing 3x less - * power for the same amount of work! (This is primarily true for Nexus 5x/6p devices, and any - * other OEM with specialized kernel logic; it is not true for Pixels.) - * - *

See b/25246923 for more. - */ - LOW_POWER, - - /** - * Better performance at the expense of power and execution of other tasks. If the user will be - * waiting for your work to complete (i.e. staring at a spinner), then this is a good choice. - * Often this is the case for handling client app requests. Otherwise, it's best to use {@link - * #LOW_POWER}. - * - *

Tasks with this priority will be allowed to contend with other urgent things across the - * system. On devices with big.LITTLE CPU architectures running Marshmallow, your tasks will be - * allowed to schedule on the big cores, executing faster but drawing 3x more power for the same - * amount of work. - * - *

Some tasks require even higher priority, such as some UI work or real-time audio. If needed, - * create your own thread(s) with an elevated priority. Please be careful! - * - *

See b/25246923 for more. - */ - HIGH_SPEED -} diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index a22bf487c22..05da2aaa9ad 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* [fixed] Fixed deadlock when handling simultaneous messages. + # 23.1.0 * [unchanged] Updated to accommodate the release of the updated [messaging_longer] Kotlin extensions library. diff --git a/firebase-messaging/firebase-messaging.gradle b/firebase-messaging/firebase-messaging.gradle index a220aae3c1f..93a4a9994c5 100644 --- a/firebase-messaging/firebase-messaging.gradle +++ b/firebase-messaging/firebase-messaging.gradle @@ -57,11 +57,6 @@ android { timeOutInMs 60 * 1000 } - - lintOptions { - abortOnError false - } - compileSdkVersion project.targetSdkVersion defaultConfig { minSdkVersion 19 diff --git a/firebase-messaging/gradle.properties b/firebase-messaging/gradle.properties index 5a4462bd8d0..c813b4b050e 100644 --- a/firebase-messaging/gradle.properties +++ b/firebase-messaging/gradle.properties @@ -1,3 +1,3 @@ -version=23.0.9 -latestReleasedVersion=23.0.8 +version=23.1.1 +latestReleasedVersion=23.1.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-messaging/ktx/src/main/kotlin/com/google/firebase/messaging/ktx/Messaging.kt b/firebase-messaging/ktx/src/main/kotlin/com/google/firebase/messaging/ktx/Messaging.kt index e40198a0132..47f80f32751 100644 --- a/firebase-messaging/ktx/src/main/kotlin/com/google/firebase/messaging/ktx/Messaging.kt +++ b/firebase-messaging/ktx/src/main/kotlin/com/google/firebase/messaging/ktx/Messaging.kt @@ -26,17 +26,20 @@ internal const val LIBRARY_NAME: String = "fire-fcm-ktx" /** Returns the [FirebaseMessaging] instance of the default [FirebaseApp]. */ val Firebase.messaging: FirebaseMessaging - get() = FirebaseMessaging.getInstance() + get() = FirebaseMessaging.getInstance() /** Returns a [RemoteMessage] instance initialized using the [init] function. */ -inline fun remoteMessage(to: String, crossinline init: RemoteMessage.Builder.() -> Unit): RemoteMessage { - val builder = RemoteMessage.Builder(to) - builder.init() - return builder.build() +inline fun remoteMessage( + to: String, + crossinline init: RemoteMessage.Builder.() -> Unit +): RemoteMessage { + val builder = RemoteMessage.Builder(to) + builder.init() + return builder.build() } /** @suppress */ class FirebaseMessagingKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-messaging/ktx/src/test/kotlin/com/google/firebase/messaging/ktx/MessagingTests.kt b/firebase-messaging/ktx/src/test/kotlin/com/google/firebase/messaging/ktx/MessagingTests.kt index f8a269bfbac..aa9e2e59806 100644 --- a/firebase-messaging/ktx/src/test/kotlin/com/google/firebase/messaging/ktx/MessagingTests.kt +++ b/firebase-messaging/ktx/src/test/kotlin/com/google/firebase/messaging/ktx/MessagingTests.kt @@ -33,59 +33,60 @@ const val APP_ID = "APP_ID" const val API_KEY = "API_KEY" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + } - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class MessagingTests : BaseTestCase() { - @Test - fun `messaging should delegate to FirebaseMessaging#getInstance()`() { - assertThat(Firebase.messaging).isSameInstanceAs(FirebaseMessaging.getInstance()) - } + @Test + fun `messaging should delegate to FirebaseMessaging#getInstance()`() { + assertThat(Firebase.messaging).isSameInstanceAs(FirebaseMessaging.getInstance()) + } - @Test - fun `remoteMessage() should produce correct RemoteMessages`() { - val recipient = "recipient" - val expectedCollapseKey = "collapse" - val msgId = "id" - val msgType = "type" - val timeToLive = 100 - val remoteMessage = remoteMessage(recipient) { - collapseKey = expectedCollapseKey - messageId = msgId - messageType = msgType - ttl = timeToLive - addData("hello", "world") - } - assertThat(remoteMessage.to).isEqualTo(recipient) - assertThat(remoteMessage.collapseKey).isEqualTo(expectedCollapseKey) - assertThat(remoteMessage.messageId).isEqualTo(msgId) - assertThat(remoteMessage.messageType).isEqualTo(msgType) - assertThat(remoteMessage.data).isEqualTo(mapOf("hello" to "world")) - } + @Test + fun `remoteMessage() should produce correct RemoteMessages`() { + val recipient = "recipient" + val expectedCollapseKey = "collapse" + val msgId = "id" + val msgType = "type" + val timeToLive = 100 + val remoteMessage = + remoteMessage(recipient) { + collapseKey = expectedCollapseKey + messageId = msgId + messageType = msgType + ttl = timeToLive + addData("hello", "world") + } + assertThat(remoteMessage.to).isEqualTo(recipient) + assertThat(remoteMessage.collapseKey).isEqualTo(expectedCollapseKey) + assertThat(remoteMessage.messageId).isEqualTo(msgId) + assertThat(remoteMessage.messageType).isEqualTo(msgType) + assertThat(remoteMessage.data).isEqualTo(mapOf("hello" to "world")) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmExecutors.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmExecutors.java index 90bfae54107..0f1bf92f062 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmExecutors.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmExecutors.java @@ -15,6 +15,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; +import android.annotation.SuppressLint; import com.google.android.gms.common.util.concurrent.NamedThreadFactory; import com.google.firebase.messaging.threads.PoolableExecutors; import com.google.firebase.messaging.threads.ThreadPriority; @@ -54,6 +55,8 @@ static Executor newFileIOExecutor() { } @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static Executor newCachedSingleThreadExecutor(String threadName) { // Creates a single threaded executor that only keeps the thread alive for a short time when // idle to reduce resource use. @@ -68,27 +71,37 @@ private static Executor newCachedSingleThreadExecutor(String threadName) { /** Creates a single threaded ScheduledPoolExecutor. */ @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ScheduledExecutorService newTopicsSyncExecutor() { return new ScheduledThreadPoolExecutor( /* corePoolSize= */ 1, new NamedThreadFactory(THREAD_TOPICS_IO)); } @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ExecutorService newNetworkIOExecutor() { // TODO(b/148493968): consider use PoolableExecutors for all FCM threading return Executors.newSingleThreadExecutor(new NamedThreadFactory(THREAD_NETWORK_IO)); } @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ExecutorService newTaskExecutor() { return Executors.newSingleThreadExecutor(new NamedThreadFactory(THREAD_TASK)); } @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ExecutorService newFileExecutor() { return Executors.newSingleThreadExecutor(new NamedThreadFactory(THREAD_FILE)); } + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ExecutorService newIntentHandleExecutor() { return PoolableExecutors.factory() .newSingleThreadExecutor( @@ -97,6 +110,8 @@ static ExecutorService newIntentHandleExecutor() { /** Creates a single threaded ScheduledPoolExecutor. */ @SuppressWarnings("ThreadChecker") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static ScheduledExecutorService newInitExecutor() { return new ScheduledThreadPoolExecutor( /* corePoolSize= */ 1, new NamedThreadFactory(THREAD_INIT)); diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmLifecycleCallbacks.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmLifecycleCallbacks.java index f14b8b0a6e8..530cdeabc02 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmLifecycleCallbacks.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/FcmLifecycleCallbacks.java @@ -15,6 +15,7 @@ import static com.google.firebase.messaging.Constants.TAG; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.content.Intent; @@ -34,6 +35,8 @@ class FcmLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { private final Set seenIntents = Collections.newSetFromMap(new WeakHashMap()); + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") @Override public void onActivityCreated(Activity createdActivity, Bundle instanceState) { Intent startingIntent = createdActivity.getIntent(); diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java index ac9dedb21ce..fc23956e6e5 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java @@ -367,6 +367,7 @@ public boolean isNotificationDelegationEnabled() { * @param enable Whether to enable or disable notification delegation. * @return A Task that completes when the notification delegation has been set. */ + @NonNull public Task setNotificationDelegationEnabled(boolean enable) { return ProxyNotificationInitializer.setEnableProxyNotification(initExecutor, context, enable); } @@ -455,6 +456,8 @@ public Task deleteToken() { * "[a-zA-Z0-9-_.~%]{1,900}". * @return A task that will be completed when the topic has been successfully subscribed to. */ + // TODO(b/261013992): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull public Task subscribeToTopic(@NonNull String topic) { return topicsSubscriberTask.onSuccessTask( @@ -470,6 +473,8 @@ public Task subscribeToTopic(@NonNull String topic) { * expression: "[a-zA-Z0-9-_.~%]{1,900}". * @return A task that will be completed when the topic has been successfully unsubscribed from. */ + // TODO(b/261013992): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull public Task unsubscribeFromTopic(@NonNull String topic) { return topicsSubscriberTask.onSuccessTask( @@ -548,6 +553,8 @@ synchronized void syncWithDelaySecondsInternal(long delaySeconds) { } @SuppressWarnings("FutureReturnValueIgnored") + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") void enqueueTaskWithDelaySeconds(Runnable task, long delaySeconds) { synchronized (FirebaseMessaging.class) { if (syncExecutor == null) { diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java index 55d842a00a5..77230e96ab8 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/SyncTask.java @@ -44,6 +44,8 @@ class SyncTask implements Runnable { private final FirebaseMessaging firebaseMessaging; @VisibleForTesting + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") ExecutorService processorExecutor = new ThreadPoolExecutor( /* corePoolSize= */ 0, diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/WakeLockHolder.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/WakeLockHolder.java index 84001d9f87d..9b80a0cbe2e 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/WakeLockHolder.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/WakeLockHolder.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.messaging; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -95,6 +96,8 @@ static ComponentName startWakefulService(@NonNull Context context, @NonNull Inte * @param connection ServiceConnection to send the Intent to. * @param intent Intent for starting the service. */ + // TODO(b/261013992): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") static void sendWakefulServiceIntent( Context context, WithinAppServiceConnection connection, Intent intent) { synchronized (syncObject) { @@ -108,9 +111,7 @@ static void sendWakefulServiceIntent( wakeLock.acquire(WAKE_LOCK_ACQUIRE_TIMEOUT_MILLIS); } - connection - .sendIntent(intent) - .addOnCompleteListener(Runnable::run, t -> completeWakefulIntent(intent)); + connection.sendIntent(intent).addOnCompleteListener(t -> completeWakefulIntent(intent)); } } diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java index 3f2b7905431..ca0740b000f 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/WithinAppServiceConnection.java @@ -16,6 +16,7 @@ import static com.google.firebase.messaging.FirebaseMessaging.TAG; import static com.google.firebase.messaging.WakeLockHolder.WAKE_LOCK_ACQUIRE_TIMEOUT_MILLIS; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -104,6 +105,8 @@ void finish() { @GuardedBy("this") private boolean connectionInProgress = false; + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") WithinAppServiceConnection(Context context, String action) { // Class instances are owned by a static variable in FirebaseInstanceIdReceiver // and GcmReceiver so that they survive getting gc'd and reinstantiated, so use a diff --git a/firebase-messaging/src/main/java/com/google/firebase/messaging/threads/PoolableExecutors.java b/firebase-messaging/src/main/java/com/google/firebase/messaging/threads/PoolableExecutors.java index 18e50fa0373..dabdecbf795 100644 --- a/firebase-messaging/src/main/java/com/google/firebase/messaging/threads/PoolableExecutors.java +++ b/firebase-messaging/src/main/java/com/google/firebase/messaging/threads/PoolableExecutors.java @@ -14,6 +14,7 @@ package com.google.firebase.messaging.threads; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import com.google.errorprone.annotations.CompileTimeConstant; import java.util.concurrent.ExecutorService; @@ -52,6 +53,8 @@ private static class DefaultExecutorFactory implements ExecutorFactory { /** {@inheritDoc} */ @NonNull @Override + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ExecutorService newThreadPool(ThreadPriority priority) { // NOTE: Cached threadpools automatically time out all threads. They have no concept of core // threads; the queue blocks until a thread is started. @@ -61,6 +64,8 @@ public ExecutorService newThreadPool(ThreadPriority priority) { /** {@inheritDoc} */ @NonNull @Override + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ExecutorService newThreadPool(ThreadFactory threadFactory, ThreadPriority priority) { return Executors.unconfigurableExecutorService(Executors.newCachedThreadPool(threadFactory)); } @@ -79,6 +84,8 @@ public ExecutorService newThreadPool(int maxConcurrency, ThreadPriority priority /** {@inheritDoc} */ @NonNull @Override + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ExecutorService newThreadPool( int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority) { ThreadPoolExecutor executor = @@ -115,6 +122,8 @@ public ExecutorService newSingleThreadExecutor( /** {@inheritDoc} */ @NonNull @Override + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ScheduledExecutorService newScheduledThreadPool( int maxConcurrency, ThreadPriority priority) { // NOTE: There's no way to make a scheduled executor stop threads automatically, because @@ -128,6 +137,8 @@ public ScheduledExecutorService newScheduledThreadPool( /** {@inheritDoc} */ @NonNull @Override + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public ScheduledExecutorService newScheduledThreadPool( int maxConcurrency, ThreadFactory threadFactory, ThreadPriority priority) { return Executors.unconfigurableScheduledExecutorService( @@ -136,6 +147,8 @@ public ScheduledExecutorService newScheduledThreadPool( @Override @NonNull + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public void executeOneOff( @CompileTimeConstant final String moduleName, @CompileTimeConstant final String name, @@ -146,6 +159,8 @@ public void executeOneOff( @Override @NonNull + // TODO(b/258424124): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public Future submitOneOff( @CompileTimeConstant final String moduleName, @CompileTimeConstant final String name, diff --git a/firebase-messaging/src/test/java/com/google/firebase/messaging/MessagingAnalyticsRoboTest.java b/firebase-messaging/src/test/java/com/google/firebase/messaging/MessagingAnalyticsRoboTest.java index d0db7a07f85..5af2f4d898e 100644 --- a/firebase-messaging/src/test/java/com/google/firebase/messaging/MessagingAnalyticsRoboTest.java +++ b/firebase-messaging/src/test/java/com/google/firebase/messaging/MessagingAnalyticsRoboTest.java @@ -105,6 +105,9 @@ public void setUp() { // Create and Initialize Firelog service components context = ApplicationProvider.getApplicationContext(); + + // Reset sharedpreferences for bigquery delivery metrics export before every test. + resetPreferencesField(DELIVERY_METRICS_EXPORT_TO_BIG_QUERY_PREF); } /** If the developer didn't include Analytics and Firelog, we should not crash. */ @@ -331,6 +334,13 @@ public void testShouldExportDeliveryMetricsToBigQuery_falseManifestTrueSetter() assertPreferencesFieldWithValue(DELIVERY_METRICS_EXPORT_TO_BIG_QUERY_PREF, true); } + private void resetPreferencesField(String field) { + SharedPreferences preferences = + context.getSharedPreferences(FCM_PREFERENCES, Context.MODE_PRIVATE); + + preferences.edit().remove(field).apply(); + } + private void assertPreferencesFieldWithValue(String field, Boolean expectedValue) { SharedPreferences preferences = context.getSharedPreferences(FCM_PREFERENCES, Context.MODE_PRIVATE); diff --git a/firebase-messaging/src/test/java/com/google/firebase/messaging/WakeLockHolderRoboTest.java b/firebase-messaging/src/test/java/com/google/firebase/messaging/WakeLockHolderRoboTest.java index 60661fc6a3c..3600c6ee8fc 100644 --- a/firebase-messaging/src/test/java/com/google/firebase/messaging/WakeLockHolderRoboTest.java +++ b/firebase-messaging/src/test/java/com/google/firebase/messaging/WakeLockHolderRoboTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowPowerManager; @RunWith(RobolectricTestRunner.class) @@ -77,6 +78,7 @@ public void testSendWakefulServiceIntent_ReleasesWakeLock() { WakeLock wakeLock = ShadowPowerManager.getLatestWakeLock(); taskCompletionSource.setResult(null); + ShadowLooper.idleMainLooper(); assertThat(wakeLock.isHeld()).isFalse(); } diff --git a/firebase-ml-modeldownloader/CHANGELOG.md b/firebase-ml-modeldownloader/CHANGELOG.md index 1995e3e58ff..9a6b9c8ea6f 100644 --- a/firebase-ml-modeldownloader/CHANGELOG.md +++ b/firebase-ml-modeldownloader/CHANGELOG.md @@ -1,5 +1,17 @@ # Unreleased +- [changed] Internal infrastructure improvements. + +# 24.1.1 +* [fixed] Fixed an issue where `FirebaseModelDownloader.getModel` was throwing + `FirebaseMlException.PERMISSION_DENIED` when the model name was empty. It now + throws `FirebaseMlException.INVALID_ARGUMENT` + (#4157) + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-ml-modeldownloader` library. The Kotlin extensions library has no +additional updates. # 24.1.0 * [unchanged] Updated to accommodate the release of the updated [firebase_ml] Kotlin extensions library. diff --git a/firebase-ml-modeldownloader/firebase-ml-modeldownloader.gradle b/firebase-ml-modeldownloader/firebase-ml-modeldownloader.gradle index efdc6e389c8..0c9ef7a4374 100644 --- a/firebase-ml-modeldownloader/firebase-ml-modeldownloader.gradle +++ b/firebase-ml-modeldownloader/firebase-ml-modeldownloader.gradle @@ -14,6 +14,7 @@ plugins { id 'firebase-library' + id 'firebase-vendor' id 'com.google.protobuf' } @@ -63,6 +64,7 @@ dependencies { implementation project(':encoders:firebase-encoders-json') implementation project(':firebase-common') implementation project(':firebase-components') + implementation project(':firebase-annotations') implementation project(':firebase-datatransport') implementation project(':firebase-installations-interop') implementation project(':transport:transport-api') @@ -76,10 +78,17 @@ dependencies { implementation 'com.google.auto.service:auto-service-annotations:1.0-rc6' implementation 'javax.inject:javax.inject:1' + implementation 'javax.inject:javax.inject:1' + vendor ('com.google.dagger:dagger:2.43.2') { + exclude group: "javax.inject", module: "javax.inject" + } + annotationProcessor 'com.google.dagger:dagger-compiler:2.43.2' + compileOnly "com.google.auto.value:auto-value-annotations:1.6.6" annotationProcessor "com.google.auto.value:auto-value:1.6.5" annotationProcessor project(":encoders:firebase-encoders-processor") + testImplementation(project(":integ-testing")) testImplementation 'androidx.test:core:1.3.0' testImplementation 'com.github.tomakehurst:wiremock-standalone:2.26.3' testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-ml-modeldownloader/gradle.properties b/firebase-ml-modeldownloader/gradle.properties index ef7100452f9..f2fd0558472 100644 --- a/firebase-ml-modeldownloader/gradle.properties +++ b/firebase-ml-modeldownloader/gradle.properties @@ -1 +1 @@ -version=24.0.6 +version=24.1.2 diff --git a/firebase-ml-modeldownloader/ktx/ktx.gradle b/firebase-ml-modeldownloader/ktx/ktx.gradle index 396d946788e..849310f14cb 100644 --- a/firebase-ml-modeldownloader/ktx/ktx.gradle +++ b/firebase-ml-modeldownloader/ktx/ktx.gradle @@ -49,6 +49,9 @@ dependencies { implementation project(':firebase-ml-modeldownloader') testImplementation "org.robolectric:robolectric:$robolectricVersion" - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation 'org.mockito:mockito-core:3.6.0' + testImplementation 'androidx.test:runner:1.5.1' + testImplementation 'androidx.test.ext:junit:1.1.4' } diff --git a/firebase-ml-modeldownloader/ktx/src/main/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloader.kt b/firebase-ml-modeldownloader/ktx/src/main/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloader.kt index aad6f58d1cd..10299f8cce9 100644 --- a/firebase-ml-modeldownloader/ktx/src/main/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloader.kt +++ b/firebase-ml-modeldownloader/ktx/src/main/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloader.kt @@ -28,18 +28,18 @@ internal const val LIBRARY_NAME: String = "firebase-ml-modeldownloader-ktx" /** Returns the [FirebaseModelDownloader] instance of the default [FirebaseApp]. */ val Firebase.modelDownloader: FirebaseModelDownloader - get() = FirebaseModelDownloader.getInstance() + get() = FirebaseModelDownloader.getInstance() /** Returns the [FirebaseModelDownloader] instance of a given [FirebaseApp]. */ fun Firebase.modelDownloader(app: FirebaseApp) = FirebaseModelDownloader.getInstance(app) /** Returns a [CustomModelDownloadConditions] initialized using the [init] function. */ fun customModelDownloadConditions( - init: CustomModelDownloadConditions.Builder.() -> Unit + init: CustomModelDownloadConditions.Builder.() -> Unit ): CustomModelDownloadConditions { - val builder = CustomModelDownloadConditions.Builder() - builder.init() - return builder.build() + val builder = CustomModelDownloadConditions.Builder() + builder.init() + return builder.build() } operator fun CustomModel.component1(): File? = file @@ -54,6 +54,6 @@ operator fun CustomModel.component5() = name /** @suppress */ class FirebaseMlModelDownloaderKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-ml-modeldownloader/ktx/src/test/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloaderTests.kt b/firebase-ml-modeldownloader/ktx/src/test/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloaderTests.kt index 7e3a6ee1049..b2d8be7eab9 100644 --- a/firebase-ml-modeldownloader/ktx/src/test/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloaderTests.kt +++ b/firebase-ml-modeldownloader/ktx/src/test/kotlin/com/google/firebase/ml/modeldownloader/ktx/ModelDownloaderTests.kt @@ -14,13 +14,14 @@ package com.google.firebase.ml.modeldownloader.ktx +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.app import com.google.firebase.ktx.initialize -import com.google.firebase.ml.modeldownloader.CustomModel import com.google.firebase.ml.modeldownloader.FirebaseModelDownloader import com.google.firebase.platforminfo.UserAgentPublisher import org.junit.After @@ -28,7 +29,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment const val APP_ID = "1:1234567890:android:321abc456def7890" const val API_KEY = "AIzaSyDOCAbC123dEf456GhI789jKl012-MnO" @@ -36,83 +36,86 @@ const val API_KEY = "AIzaSyDOCAbC123dEf456GhI789jKl012-MnO" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - RuntimeEnvironment.application, - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - Firebase.initialize( - RuntimeEnvironment.application, - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } -@RunWith(RobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) class ModelDownloaderTests : BaseTestCase() { - @Test - fun `modelDownloader should delegate to FirebaseModelDownloader#getInstance()`() { - assertThat(Firebase.modelDownloader).isSameInstanceAs(FirebaseModelDownloader.getInstance()) + @Test + fun `modelDownloader should delegate to FirebaseModelDownloader#getInstance()`() { + assertThat(Firebase.modelDownloader).isSameInstanceAs(FirebaseModelDownloader.getInstance()) + } + + @Test + fun `Firebase#modelDownloader(FirebaseApp) should delegate to FirebaseModelDownloader#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.modelDownloader(app)) + .isSameInstanceAs(FirebaseModelDownloader.getInstance(app)) + } + + @Test + fun `CustomModelDownloadConditions builder works`() { + val conditions = customModelDownloadConditions { + requireCharging() + requireDeviceIdle() } - @Test - fun `Firebase#modelDownloader(FirebaseApp) should delegate to FirebaseModelDownloader#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.modelDownloader(app)) - .isSameInstanceAs(FirebaseModelDownloader.getInstance(app)) - } + assertThat(conditions.isChargingRequired).isEqualTo(true) + assertThat(conditions.isWifiRequired).isEqualTo(false) + assertThat(conditions.isDeviceIdleRequired).isEqualTo(true) + } - @Test - fun `CustomModelDownloadConditions builder works`() { - val conditions = customModelDownloadConditions { - requireCharging() - requireDeviceIdle() - } + @Test + fun `CustomModel destructuring declarations work`() { + val app = Firebase.app(EXISTING_APP) - assertThat(conditions.isChargingRequired).isEqualTo(true) - assertThat(conditions.isWifiRequired).isEqualTo(false) - assertThat(conditions.isDeviceIdleRequired).isEqualTo(true) - } - - @Test - fun `CustomModel destructuring declarations work`() { - val modelName = "myModel" - val modelHash = "someHash" - val fileSize = 200L - val downloadId = 258L + val modelName = "myModel" + val modelHash = "someHash" + val fileSize = 200L + val downloadId = 258L - val customModel = CustomModel(modelName, modelHash, fileSize, downloadId) + val customModel = + Firebase.modelDownloader(app).modelFactory.create(modelName, modelHash, fileSize, downloadId) - val (file, size, id, hash, name) = customModel + val (_, size, id, hash, name) = customModel - assertThat(name).isEqualTo(customModel.name) - assertThat(hash).isEqualTo(customModel.modelHash) - assertThat(size).isEqualTo(customModel.size) - assertThat(id).isEqualTo(customModel.downloadId) - } + assertThat(name).isEqualTo(customModel.name) + assertThat(hash).isEqualTo(customModel.modelHash) + assertThat(size).isEqualTo(customModel.size) + assertThat(id).isEqualTo(customModel.downloadId) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-ml-modeldownloader/ml-data-collection-tests/src/test/java/com/google/firebase/ml_data_collection_tests/MlDataCollectionTestUtil.java b/firebase-ml-modeldownloader/ml-data-collection-tests/src/test/java/com/google/firebase/ml_data_collection_tests/MlDataCollectionTestUtil.java index cca82b8e6f0..2b9788e8d84 100644 --- a/firebase-ml-modeldownloader/ml-data-collection-tests/src/test/java/com/google/firebase/ml_data_collection_tests/MlDataCollectionTestUtil.java +++ b/firebase-ml-modeldownloader/ml-data-collection-tests/src/test/java/com/google/firebase/ml_data_collection_tests/MlDataCollectionTestUtil.java @@ -14,11 +14,10 @@ package com.google.firebase.ml_data_collection_tests; -import android.content.Context; -import android.content.SharedPreferences; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.ml.modeldownloader.FirebaseModelDownloader; import com.google.firebase.ml.modeldownloader.internal.SharedPreferencesUtil; import java.util.function.Consumer; @@ -47,12 +46,8 @@ static void withApp(String name, Consumer callable) { } static SharedPreferencesUtil getSharedPreferencesUtil(FirebaseApp app) { - return new SharedPreferencesUtil(app); - } - - static SharedPreferences getSharedPreferences(FirebaseApp app) { - return app.getApplicationContext() - .getSharedPreferences(SharedPreferencesUtil.PREFERENCES_PACKAGE_NAME, Context.MODE_PRIVATE); + return new SharedPreferencesUtil( + app, FirebaseModelDownloader.getInstance(app).getModelFactory()); } static void setSharedPreferencesTo(FirebaseApp app, Boolean enabled) { diff --git a/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestGetModelLocal.java b/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestGetModelLocal.java index 1febf4a9785..8fa23f00bb5 100644 --- a/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestGetModelLocal.java +++ b/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestGetModelLocal.java @@ -45,20 +45,25 @@ public class TestGetModelLocal { private static final String MODEL_NAME_LOCAL = "getLocalModel"; private static final String MODEL_NAME_LOCAL_2 = "getLocalModel2"; private static final String MODEL_HASH = "origHash324"; - private final CustomModel SETUP_LOADED_LOCAL_MODEL = - new CustomModel(MODEL_NAME_LOCAL, MODEL_HASH, 100, 0); private FirebaseApp app; private File firstDeviceModelFile; private File firstLoadTempModelFile; + private CustomModel.Factory modelFactory; + private CustomModel setupLoadedLocalModel; + @Before public void before() { app = FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()); app.setDataCollectionDefaultEnabled(Boolean.FALSE); FirebaseModelDownloader firebaseModelDownloader = FirebaseModelDownloader.getInstance(app); - SharedPreferencesUtil sharedPreferencesUtil = new SharedPreferencesUtil(app); + modelFactory = firebaseModelDownloader.getModelFactory(); + + setupLoadedLocalModel = modelFactory.create(MODEL_NAME_LOCAL, MODEL_HASH, 100, 0); + + SharedPreferencesUtil sharedPreferencesUtil = new SharedPreferencesUtil(app, modelFactory); // reset shared preferences and downloads for models used by this test. firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME_LOCAL); firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME_LOCAL_2); @@ -79,7 +84,11 @@ public void teardown() { } private void setUpLoadedLocalModelWithFile() throws Exception { - ModelFileManager fileManager = ModelFileManager.getInstance(); + ModelFileManager fileManager = + new ModelFileManager( + app.getApplicationContext(), + app.getPersistenceKey(), + new SharedPreferencesUtil(app, modelFactory)); final File testDir = new File(app.getApplicationContext().getNoBackupFilesDir(), "tmpModels"); testDir.mkdirs(); // make sure the directory is empty. Doesn't recurse into subdirs, but that's OK since @@ -105,14 +114,14 @@ private void setUpLoadedLocalModelWithFile() throws Exception { ParcelFileDescriptor fd = ParcelFileDescriptor.open(firstLoadTempModelFile, ParcelFileDescriptor.MODE_READ_ONLY); - firstDeviceModelFile = fileManager.moveModelToDestinationFolder(SETUP_LOADED_LOCAL_MODEL, fd); + firstDeviceModelFile = fileManager.moveModelToDestinationFolder(setupLoadedLocalModel, fd); assertEquals(firstDeviceModelFile, new File(expectedDestinationFolder + "/0")); assertTrue(firstDeviceModelFile.exists()); fd.close(); fakePreloadedCustomModel( MODEL_NAME_LOCAL, - SETUP_LOADED_LOCAL_MODEL.getModelHash(), + setupLoadedLocalModel.getModelHash(), 99, expectedDestinationFolder + "/0"); } @@ -228,9 +237,9 @@ public void localModel_preloadedDoNotFetchUpdate() throws Exception { } private void fakePreloadedCustomModel(String modelName, String hash, long size, String filePath) { - SharedPreferencesUtil sharedPreferencesUtil = new SharedPreferencesUtil(app); + SharedPreferencesUtil sharedPreferencesUtil = new SharedPreferencesUtil(app, modelFactory); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(modelName, hash, size, 0L, filePath)); + modelFactory.create(modelName, hash, size, 0L, filePath)); } private Set getDownloadedModelList() diff --git a/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestPublicApi.java b/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestPublicApi.java index 52f58a32137..f1dfbf2a841 100644 --- a/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestPublicApi.java +++ b/firebase-ml-modeldownloader/src/androidTest/java/com/google/firebase/ml/modeldownloader/TestPublicApi.java @@ -45,7 +45,8 @@ public void before() { app.setDataCollectionDefaultEnabled(Boolean.FALSE); FirebaseModelDownloader firebaseModelDownloader = FirebaseModelDownloader.getInstance(app); - SharedPreferencesUtil sharedPreferencesUtil = new SharedPreferencesUtil(app); + SharedPreferencesUtil sharedPreferencesUtil = + new SharedPreferencesUtil(app, firebaseModelDownloader.getModelFactory()); // reset shared preferences and downloads for models used by this test. firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME_LOCAL); firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME_LOCAL_2); diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/CustomModel.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/CustomModel.java index 2395161ed98..16d9c32c6f0 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/CustomModel.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/CustomModel.java @@ -16,9 +16,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import com.google.android.gms.common.internal.Objects; import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; import java.io.File; /** @@ -27,6 +31,7 @@ * downloaded, the original model file will be removed once it is safe to do so. */ public class CustomModel { + private final ModelFileDownloadService fileDownloadService; private final String name; private final long downloadId; private final long fileSize; @@ -35,57 +40,31 @@ public class CustomModel { private final String downloadUrl; private final long downloadUrlExpiry; - /** - * Use when creating a custom model while the initial download is still in progress. - * - * @param name Model name. - * @param modelHash Model hash. - * @param fileSize Model file size. - * @param downloadId Android Download Manger - download ID. - * @hide - */ - public CustomModel( - @NonNull String name, @NonNull String modelHash, long fileSize, long downloadId) { - this(name, modelHash, fileSize, downloadId, "", "", 0); - } + /** @hide */ + @AssistedFactory + public interface Factory { + CustomModel create( + @Assisted("name") String name, + @Assisted("modelHash") String modelHash, + @Assisted("fileSize") long fileSize, + @Assisted("downloadId") long downloadId, + @Assisted("localFilePath") String localFilePath, + @Assisted("downloadUrl") String downloadUrl, + @Assisted("downloadUrlExpiry") long downloadUrlExpiry); - /** - * Use when creating a custom model from a stored model with a new download in the background. - * - * @param name Model name. - * @param modelHash Model hash. - * @param fileSize Model file size. - * @param downloadId Android Download Manger - download ID. - * @hide - */ - public CustomModel( - @NonNull String name, - @NonNull String modelHash, - long fileSize, - long downloadId, - String localFilePath) { - this(name, modelHash, fileSize, downloadId, localFilePath, "", 0); - } + default CustomModel create(String name, String modelHash, long fileSize, long downloadId) { + return create(name, modelHash, fileSize, downloadId, "", "", 0); + } - /** - * Use when creating a custom model from a download service response. Download URL and download - * URL expiry should go together. These will not be stored in user preferences as this is a - * temporary step towards setting the actual download ID. - * - * @param name Model name. - * @param modelHash Model hash. - * @param fileSize Model file size. - * @param downloadUrl Download URL path - * @param downloadUrlExpiry Time download URL path expires. - * @hide - */ - public CustomModel( - @NonNull String name, - @NonNull String modelHash, - long fileSize, - String downloadUrl, - long downloadUrlExpiry) { - this(name, modelHash, fileSize, 0, "", downloadUrl, downloadUrlExpiry); + default CustomModel create( + String name, String modelHash, long fileSize, long downloadId, String localFilePath) { + return create(name, modelHash, fileSize, downloadId, localFilePath, "", 0); + } + + default CustomModel create( + String name, String modelHash, long fileSize, String downloadUrl, long downloadUrlExpiry) { + return create(name, modelHash, fileSize, 0, "", downloadUrl, downloadUrlExpiry); + } } /** @@ -100,14 +79,19 @@ public CustomModel( * @param downloadUrlExpiry Expiry time of download URL link. * @hide */ - private CustomModel( - @NonNull String name, - @NonNull String modelHash, - long fileSize, - long downloadId, - @Nullable String localFilePath, - @Nullable String downloadUrl, - long downloadUrlExpiry) { + @AssistedInject + @VisibleForTesting + @RestrictTo(RestrictTo.Scope.LIBRARY) + public CustomModel( + ModelFileDownloadService fileDownloadService, + @Assisted("name") String name, + @Assisted("modelHash") String modelHash, + @Assisted("fileSize") long fileSize, + @Assisted("downloadId") long downloadId, + @Assisted("localFilePath") String localFilePath, + @Assisted("downloadUrl") String downloadUrl, + @Assisted("downloadUrlExpiry") long downloadUrlExpiry) { + this.fileDownloadService = fileDownloadService; this.modelHash = modelHash; this.name = name; this.fileSize = fileSize; @@ -137,7 +121,7 @@ public String getName() { */ @Nullable public File getFile() { - return getFile(ModelFileDownloadService.getInstance()); + return getFile(fileDownloadService); } /** diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloader.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloader.java index cbf8fb1f894..3309902f98f 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloader.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloader.java @@ -25,7 +25,8 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; -import com.google.firebase.installations.FirebaseInstallationsApi; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.ml.modeldownloader.internal.CustomModelDownloadService; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.DownloadStatus; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.ErrorCode; @@ -36,7 +37,7 @@ import java.io.File; import java.util.Set; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import javax.inject.Inject; public class FirebaseModelDownloader { @@ -46,25 +47,15 @@ public class FirebaseModelDownloader { private final ModelFileDownloadService fileDownloadService; private final ModelFileManager fileManager; private final CustomModelDownloadService modelDownloadService; - private final Executor executor; + private final Executor bgExecutor; + private final Executor blockingExecutor; private final FirebaseMlLogger eventLogger; + private final CustomModel.Factory modelFactory; - @RequiresApi(api = VERSION_CODES.KITKAT) - FirebaseModelDownloader( - FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallationsApi) { - this.firebaseOptions = firebaseApp.getOptions(); - this.sharedPreferencesUtil = new SharedPreferencesUtil(firebaseApp); - this.eventLogger = FirebaseMlLogger.getInstance(firebaseApp); - this.fileDownloadService = new ModelFileDownloadService(firebaseApp); - this.modelDownloadService = - new CustomModelDownloadService(firebaseApp, firebaseInstallationsApi); - - this.executor = Executors.newSingleThreadExecutor(); - fileManager = ModelFileManager.getInstance(); - } - + @Inject @VisibleForTesting + @RequiresApi(api = VERSION_CODES.KITKAT) FirebaseModelDownloader( FirebaseOptions firebaseOptions, SharedPreferencesUtil sharedPreferencesUtil, @@ -72,14 +63,18 @@ public class FirebaseModelDownloader { CustomModelDownloadService modelDownloadService, ModelFileManager fileManager, FirebaseMlLogger eventLogger, - Executor executor) { + @Background Executor bgExecutor, + @Blocking Executor blockingExecutor, + CustomModel.Factory modelFactory) { this.firebaseOptions = firebaseOptions; this.sharedPreferencesUtil = sharedPreferencesUtil; this.fileDownloadService = fileDownloadService; this.modelDownloadService = modelDownloadService; this.fileManager = fileManager; this.eventLogger = eventLogger; - this.executor = executor; + this.bgExecutor = bgExecutor; + this.blockingExecutor = blockingExecutor; + this.modelFactory = modelFactory; } /** @@ -214,7 +209,7 @@ private Task getCompletedLocalCustomModelTask(@NonNull CustomModel if (downloadInProgressTask != null) { return downloadInProgressTask.continueWithTask( - executor, + bgExecutor, downloadTask -> { if (downloadTask.isSuccessful()) { return finishModelDownload(model.getName()); @@ -238,7 +233,7 @@ private Task getCompletedLocalCustomModelTask(@NonNull CustomModel // bad model state - delete all existing model details and return exception return deleteDownloadedModel(model.getName()) .continueWithTask( - executor, + bgExecutor, deletionTask -> Tasks.forException( new FirebaseMlException( @@ -271,7 +266,7 @@ private Task getCustomModelTask( firebaseOptions.getProjectId(), modelName, modelHash); return incomingModelDetails.continueWithTask( - executor, + bgExecutor, incomingModelDetailTask -> { if (incomingModelDetailTask.isSuccessful()) { // null means we have the latest model or we failed to connect. @@ -355,7 +350,7 @@ && new File(currentModel.getLocalFilePath()).exists()) { return fileDownloadService .download(incomingModelDetailTask.getResult(), conditions) .continueWithTask( - executor, + blockingExecutor, downloadTask -> { if (downloadTask.isSuccessful()) { return finishModelDownload(modelName); @@ -388,14 +383,14 @@ private Task retryExpiredUrlDownload( firebaseOptions.getProjectId(), modelName); // no local model - start download. return retryModelDetails.continueWithTask( - executor, + bgExecutor, retryModelDetailTask -> { if (retryModelDetailTask.isSuccessful()) { // start download return fileDownloadService .download(retryModelDetailTask.getResult(), conditions) .continueWithTask( - executor, + bgExecutor, retryDownloadTask -> { if (retryDownloadTask.isSuccessful()) { return finishModelDownload(modelName); @@ -445,7 +440,7 @@ public Task> listDownloadedModels() { fileDownloadService.maybeCheckDownloadingComplete(); TaskCompletionSource> taskCompletionSource = new TaskCompletionSource<>(); - executor.execute( + bgExecutor.execute( () -> taskCompletionSource.setResult(sharedPreferencesUtil.listDownloadedModels())); return taskCompletionSource.getTask(); } @@ -459,7 +454,7 @@ public Task> listDownloadedModels() { public Task deleteDownloadedModel(@NonNull String modelName) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); - executor.execute( + bgExecutor.execute( () -> { // remove all files associated with this model and then clean up model references. boolean isSuccessful = deleteModelDetails(modelName); @@ -536,4 +531,10 @@ public Task getModelDownloadId( String getApplicationId() { return firebaseOptions.getApplicationId(); } + + /** @hide */ + @VisibleForTesting + public CustomModel.Factory getModelFactory() { + return modelFactory; + } } diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderRegistrar.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderRegistrar.java index cb92e0950ac..5f46ec18536 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderRegistrar.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderRegistrar.java @@ -14,23 +14,23 @@ package com.google.firebase.ml.modeldownloader; +import android.content.Context; import android.os.Build.VERSION_CODES; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.google.android.datatransport.TransportFactory; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; -import com.google.firebase.ml.modeldownloader.internal.CustomModelDownloadService; -import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogger; -import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService; -import com.google.firebase.ml.modeldownloader.internal.ModelFileManager; -import com.google.firebase.ml.modeldownloader.internal.SharedPreferencesUtil; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * Registrar for setting up Firebase ML Model Downloader's dependency injections in Firebase Android @@ -45,46 +45,28 @@ public class FirebaseModelDownloaderRegistrar implements ComponentRegistrar { @NonNull @RequiresApi(api = VERSION_CODES.KITKAT) public List> getComponents() { + Qualified bgExecutor = Qualified.qualified(Background.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); return Arrays.asList( Component.builder(FirebaseModelDownloader.class) .name(LIBRARY_NAME) + .add(Dependency.required(Context.class)) .add(Dependency.required(FirebaseApp.class)) - .add(Dependency.required(FirebaseInstallationsApi.class)) + .add(Dependency.requiredProvider(FirebaseInstallationsApi.class)) + .add(Dependency.requiredProvider(TransportFactory.class)) + .add(Dependency.required(bgExecutor)) + .add(Dependency.required(blockingExecutor)) .factory( c -> - new FirebaseModelDownloader( - c.get(FirebaseApp.class), c.get(FirebaseInstallationsApi.class))) - .build(), - Component.builder(SharedPreferencesUtil.class) - .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new SharedPreferencesUtil(c.get(FirebaseApp.class))) - .build(), - Component.builder(FirebaseMlLogger.class) - .add(Dependency.required(FirebaseApp.class)) - .add(Dependency.required(TransportFactory.class)) - .add(Dependency.required(SharedPreferencesUtil.class)) - .factory( - c -> - new FirebaseMlLogger( - c.get(FirebaseApp.class), - c.get(SharedPreferencesUtil.class), - c.get(TransportFactory.class))) - .build(), - Component.builder(ModelFileManager.class) - .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new ModelFileManager(c.get(FirebaseApp.class))) - .build(), - Component.builder(ModelFileDownloadService.class) - .add(Dependency.required(FirebaseApp.class)) - .factory(c -> new ModelFileDownloadService(c.get(FirebaseApp.class))) - .build(), - Component.builder(CustomModelDownloadService.class) - .add(Dependency.required(FirebaseApp.class)) - .add(Dependency.required(FirebaseInstallationsApi.class)) - .factory( - c -> - new CustomModelDownloadService( - c.get(FirebaseApp.class), c.get(FirebaseInstallationsApi.class))) + DaggerModelDownloaderComponent.builder() + .setApplicationContext(c.get(Context.class)) + .setFirebaseApp(c.get(FirebaseApp.class)) + .setFis(c.getProvider(FirebaseInstallationsApi.class)) + .setBlockingExecutor(c.get(blockingExecutor)) + .setBgExecutor(c.get(bgExecutor)) + .setTransportFactory(c.getProvider(TransportFactory.class)) + .build() + .getModelDownloader()) .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); } diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/ModelDownloaderComponent.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/ModelDownloaderComponent.java new file mode 100644 index 00000000000..bbf3686a38d --- /dev/null +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/ModelDownloaderComponent.java @@ -0,0 +1,98 @@ +// Copyright 2022 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.ml.modeldownloader; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Log; +import com.google.android.datatransport.TransportFactory; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.annotations.concurrent.Background; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.inject.Provider; +import com.google.firebase.installations.FirebaseInstallationsApi; +import dagger.BindsInstance; +import dagger.Component; +import dagger.Module; +import dagger.Provides; +import java.util.concurrent.Executor; +import javax.inject.Named; +import javax.inject.Singleton; + +/** @hide */ +@Component(modules = ModelDownloaderComponent.MainModule.class) +@Singleton +interface ModelDownloaderComponent { + FirebaseModelDownloader getModelDownloader(); + + @Component.Builder + interface Builder { + @BindsInstance + Builder setApplicationContext(Context context); + + @BindsInstance + Builder setFirebaseApp(FirebaseApp app); + + @BindsInstance + Builder setFis(Provider fis); + + @BindsInstance + Builder setTransportFactory(Provider transportFactory); + + @BindsInstance + Builder setBlockingExecutor(@Blocking Executor blockingExecutor); + + @BindsInstance + Builder setBgExecutor(@Background Executor bgExecutor); + + ModelDownloaderComponent build(); + } + + @Module + interface MainModule { + + @Provides + @Named("persistenceKey") + static String persistenceKey(FirebaseApp app) { + return app.getPersistenceKey(); + } + + @Provides + @Named("appPackageName") + static String appPackageName(Context applicationContext) { + return applicationContext.getPackageName(); + } + + @Provides + static FirebaseOptions firebaseOptions(FirebaseApp app) { + return app.getOptions(); + } + + @Provides + @Singleton + @Named("appVersionCode") + static String appVersionCode( + Context applicationContext, @Named("appPackageName") String appPackageName) { + try { + return String.valueOf( + applicationContext.getPackageManager().getPackageInfo(appPackageName, 0).versionCode); + } catch (PackageManager.NameNotFoundException e) { + Log.e("ModelDownloader", "Exception thrown when trying to get app version " + e); + } + return ""; + } + } +} diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadService.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadService.java index 45f4e212117..3b9f1ec8752 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadService.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadService.java @@ -16,6 +16,7 @@ import android.content.Context; import android.content.pm.PackageManager; +import android.text.TextUtils; import android.util.JsonReader; import android.util.Log; import androidx.annotation.NonNull; @@ -25,7 +26,9 @@ import com.google.android.gms.common.util.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.inject.Provider; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.installations.InstallationTokenResult; import com.google.firebase.ml.modeldownloader.CustomModel; @@ -45,9 +48,9 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.Executor; import java.util.zip.GZIPInputStream; +import javax.inject.Inject; import org.json.JSONObject; /** @@ -84,40 +87,50 @@ public class CustomModelDownloadService { @VisibleForTesting static final String DOWNLOAD_MODEL_REGEX = "%s/v1beta2/projects/%s/models/%s:download"; - private final ExecutorService executorService; - private final FirebaseInstallationsApi firebaseInstallations; + private final Provider firebaseInstallations; private final FirebaseMlLogger eventLogger; private final String apiKey; @Nullable private final String fingerprintHashForPackage; private final Context context; + private final CustomModel.Factory modelFactory; private String downloadHost = FIREBASE_DOWNLOAD_HOST; + private final Executor blockingExecutor; + @Inject public CustomModelDownloadService( - FirebaseApp firebaseApp, FirebaseInstallationsApi installationsApi) { - context = firebaseApp.getApplicationContext(); + Context context, + FirebaseOptions options, + Provider installationsApi, + FirebaseMlLogger eventLogger, + CustomModel.Factory modelFactory, + @Blocking Executor blockingExecutor) { + this.context = context; firebaseInstallations = installationsApi; - apiKey = firebaseApp.getOptions().getApiKey(); + apiKey = options.getApiKey(); fingerprintHashForPackage = getFingerprintHashForPackage(context); - executorService = Executors.newCachedThreadPool(); - this.eventLogger = FirebaseMlLogger.getInstance(); + this.blockingExecutor = blockingExecutor; + this.eventLogger = eventLogger; + this.modelFactory = modelFactory; } @VisibleForTesting CustomModelDownloadService( Context context, - FirebaseInstallationsApi firebaseInstallations, - ExecutorService executorService, + Provider firebaseInstallations, + Executor blockingExecutor, String apiKey, String fingerprintHashForPackage, String downloadHost, - FirebaseMlLogger eventLogger) { + FirebaseMlLogger eventLogger, + CustomModel.Factory modelFactory) { this.context = context; this.firebaseInstallations = firebaseInstallations; - this.executorService = executorService; + this.blockingExecutor = blockingExecutor; this.apiKey = apiKey; this.fingerprintHashForPackage = fingerprintHashForPackage; this.downloadHost = downloadHost; this.eventLogger = eventLogger; + this.modelFactory = modelFactory; } /** @@ -148,64 +161,75 @@ public Task getNewDownloadUrlWithExpiry(String projectNumber, Strin public Task getCustomModelDetails( String projectNumber, String modelName, String modelHash) { try { - URL url = - new URL(String.format(DOWNLOAD_MODEL_REGEX, downloadHost, projectNumber, modelName)); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(CONNECTION_TIME_OUT_MS); - connection.setRequestProperty(ACCEPT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - connection.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON); - if (modelHash != null && !modelHash.isEmpty()) { - connection.setRequestProperty(IF_NONE_MATCH_HEADER_KEY, modelHash); - } + + if (TextUtils.isEmpty(modelName)) + throw new FirebaseMlException( + "Error cannot retrieve model from reading an empty modelName", + FirebaseMlException.INVALID_ARGUMENT); Task installationAuthTokenTask = - firebaseInstallations.getToken(false); + firebaseInstallations.get().getToken(false); return installationAuthTokenTask.continueWithTask( - executorService, + blockingExecutor, (CustomModelTask) -> { - if (!installationAuthTokenTask.isSuccessful()) { - ErrorCode errorCode = ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED; - String errorMessage = "Failed to get model due to authentication error"; - int exceptionCode = FirebaseMlException.UNAUTHENTICATED; - if (installationAuthTokenTask.getException() != null - && (installationAuthTokenTask.getException() instanceof UnknownHostException - || installationAuthTokenTask.getException().getCause() - instanceof UnknownHostException)) { - errorCode = ErrorCode.NO_NETWORK_CONNECTION; - errorMessage = "Failed to retrieve model info due to no internet connection."; - exceptionCode = FirebaseMlException.NO_NETWORK_CONNECTION; + try { + URL url = + new URL( + String.format(DOWNLOAD_MODEL_REGEX, downloadHost, projectNumber, modelName)); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(CONNECTION_TIME_OUT_MS); + connection.setRequestProperty(ACCEPT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + connection.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON); + if (modelHash != null && !modelHash.isEmpty()) { + connection.setRequestProperty(IF_NONE_MATCH_HEADER_KEY, modelHash); + } + if (!installationAuthTokenTask.isSuccessful()) { + ErrorCode errorCode = ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED; + String errorMessage = "Failed to get model due to authentication error"; + int exceptionCode = FirebaseMlException.UNAUTHENTICATED; + if (installationAuthTokenTask.getException() != null + && (installationAuthTokenTask.getException() instanceof UnknownHostException + || installationAuthTokenTask.getException().getCause() + instanceof UnknownHostException)) { + errorCode = ErrorCode.NO_NETWORK_CONNECTION; + errorMessage = "Failed to retrieve model info due to no internet connection."; + exceptionCode = FirebaseMlException.NO_NETWORK_CONNECTION; + } + eventLogger.logDownloadFailureWithReason( + modelFactory.create(modelName, modelHash != null ? modelHash : "", 0, 0L), + false, + errorCode.getValue()); + return Tasks.forException(new FirebaseMlException(errorMessage, exceptionCode)); } - eventLogger.logDownloadFailureWithReason( - new CustomModel(modelName, modelHash != null ? modelHash : "", 0, 0L), - false, - errorCode.getValue()); - return Tasks.forException(new FirebaseMlException(errorMessage, exceptionCode)); - } - connection.setRequestProperty( - INSTALLATIONS_AUTH_TOKEN_HEADER, installationAuthTokenTask.getResult().getToken()); - connection.setRequestProperty(API_KEY_HEADER, apiKey); + connection.setRequestProperty( + INSTALLATIONS_AUTH_TOKEN_HEADER, + installationAuthTokenTask.getResult().getToken()); + connection.setRequestProperty(API_KEY_HEADER, apiKey); - // Headers required for Android API Key Restrictions. - connection.setRequestProperty(X_ANDROID_PACKAGE_HEADER, context.getPackageName()); + // Headers required for Android API Key Restrictions. + connection.setRequestProperty(X_ANDROID_PACKAGE_HEADER, context.getPackageName()); - if (fingerprintHashForPackage != null) { - connection.setRequestProperty(X_ANDROID_CERT_HEADER, fingerprintHashForPackage); - } + if (fingerprintHashForPackage != null) { + connection.setRequestProperty(X_ANDROID_CERT_HEADER, fingerprintHashForPackage); + } - return fetchDownloadDetails(modelName, connection); - }); + return fetchDownloadDetails(modelName, connection); + } catch (IOException e) { + eventLogger.logDownloadFailureWithReason( + modelFactory.create(modelName, modelHash, 0, 0L), + false, + ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED.getValue()); - } catch (IOException e) { - eventLogger.logDownloadFailureWithReason( - new CustomModel(modelName, modelHash, 0, 0L), - false, - ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED.getValue()); + return Tasks.forException( + new FirebaseMlException( + "Error reading custom model from download service: " + e.getMessage(), + FirebaseMlException.INVALID_ARGUMENT)); + } + }); - return Tasks.forException( - new FirebaseMlException( - "Error reading custom model from download service: " + e.getMessage(), - FirebaseMlException.INVALID_ARGUMENT)); + } catch (FirebaseMlException e) { + return Tasks.forException(e); } } @@ -302,7 +326,7 @@ private Task fetchDownloadDetails(String modelName, HttpURLConnecti errorMessage = "Failed to retrieve model info due to no internet connection."; exceptionCode = FirebaseMlException.NO_NETWORK_CONNECTION; } - eventLogger.logModelInfoRetrieverFailure(new CustomModel(modelName, "", 0, 0), errorCode); + eventLogger.logModelInfoRetrieverFailure(modelFactory.create(modelName, "", 0, 0), errorCode); return Tasks.forException(new FirebaseMlException(errorMessage, exceptionCode)); } } @@ -310,7 +334,7 @@ private Task fetchDownloadDetails(String modelName, HttpURLConnecti private Task setAndLogException( String modelName, int httpResponseCode, String errorMessage, @Code int invalidArgument) { eventLogger.logModelInfoRetrieverFailure( - new CustomModel(modelName, "", 0, 0), + modelFactory.create(modelName, "", 0, 0), ErrorCode.MODEL_INFO_DOWNLOAD_UNSUCCESSFUL_HTTP_STATUS, httpResponseCode); return Tasks.forException(new FirebaseMlException(errorMessage, invalidArgument)); @@ -329,7 +353,7 @@ private Task readCustomModelResponse( if (modelHash == null || modelHash.isEmpty()) { eventLogger.logDownloadFailureWithReason( - new CustomModel(modelName, modelHash, 0, 0L), + modelFactory.create(modelName, modelHash, 0, 0L), false, ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED.getValue()); return Tasks.forException( @@ -363,12 +387,13 @@ private Task readCustomModelResponse( inputStream.close(); if (!downloadUrl.isEmpty() && expireTime > 0L) { - CustomModel model = new CustomModel(modelName, modelHash, fileSize, downloadUrl, expireTime); + CustomModel model = + modelFactory.create(modelName, modelHash, fileSize, downloadUrl, expireTime); eventLogger.logModelInfoRetrieverSuccess(model); return Tasks.forResult(model); } eventLogger.logDownloadFailureWithReason( - new CustomModel(modelName, modelHash, 0, 0L), + modelFactory.create(modelName, modelHash, 0, 0L), false, ErrorCode.MODEL_INFO_DOWNLOAD_CONNECTION_FAILED.getValue()); return Tasks.forException( diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSender.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSender.java index 8dbae13bd29..ca6ec9d65c1 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSender.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSender.java @@ -19,6 +19,10 @@ import com.google.android.datatransport.Event; import com.google.android.datatransport.Transport; import com.google.android.datatransport.TransportFactory; +import com.google.firebase.components.Lazy; +import com.google.firebase.inject.Provider; +import javax.inject.Inject; +import javax.inject.Singleton; /** * This class is responsible for sending Firebase ML Log Events to Firebase through Google @@ -28,26 +32,26 @@ * * @hide */ +@Singleton public class DataTransportMlEventSender { private static final String FIREBASE_ML_LOG_SDK_NAME = "FIREBASE_ML_LOG_SDK"; - private final Transport transport; + private final Provider> transport; - @NonNull - public static DataTransportMlEventSender create(TransportFactory transportFactory) { - final Transport transport = - transportFactory.getTransport( - FIREBASE_ML_LOG_SDK_NAME, - FirebaseMlLogEvent.class, - Encoding.of("json"), - FirebaseMlLogEvent.getFirebaseMlJsonTransformer()); - return new DataTransportMlEventSender(transport); - } - - DataTransportMlEventSender(Transport transport) { - this.transport = transport; + @Inject + DataTransportMlEventSender(Provider transportFactory) { + this.transport = + new Lazy<>( + () -> + transportFactory + .get() + .getTransport( + FIREBASE_ML_LOG_SDK_NAME, + FirebaseMlLogEvent.class, + Encoding.of("json"), + FirebaseMlLogEvent.getFirebaseMlJsonTransformer())); } public void sendEvent(@NonNull FirebaseMlLogEvent firebaseMlLogEvent) { - transport.send(Event.ofData(firebaseMlLogEvent)); + transport.get().send(Event.ofData(firebaseMlLogEvent)); } } diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLogger.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLogger.java index b68b61bf829..651fe901370 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLogger.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLogger.java @@ -14,15 +14,11 @@ package com.google.firebase.ml.modeldownloader.internal; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import com.google.android.datatransport.TransportFactory; -import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; import com.google.firebase.ml.modeldownloader.BuildConfig; import com.google.firebase.ml.modeldownloader.CustomModel; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.DeleteModelLogEvent; @@ -33,6 +29,10 @@ import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.ModelOptions; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.ModelOptions.ModelInfo; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.SystemInfo; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; /** * Logging class for Firebase ML Event logging. @@ -40,62 +40,35 @@ * @hide */ @WorkerThread +@Singleton public class FirebaseMlLogger { public static final int NO_FAILURE_VALUE = 0; private static final String TAG = "FirebaseMlLogger"; private final SharedPreferencesUtil sharedPreferencesUtil; private final DataTransportMlEventSender eventSender; - private final FirebaseApp firebaseApp; + private final FirebaseOptions firebaseOptions; - private final String appPackageName; - private final String appVersion; + private final Provider appPackageName; + private final Provider appVersionCode; private final String firebaseProjectId; private final String apiKey; + @Inject public FirebaseMlLogger( - @NonNull FirebaseApp firebaseApp, - @NonNull SharedPreferencesUtil sharedPreferencesUtil, - @NonNull TransportFactory transportFactory) { - this.firebaseApp = firebaseApp; - this.sharedPreferencesUtil = sharedPreferencesUtil; - this.eventSender = DataTransportMlEventSender.create(transportFactory); - - this.firebaseProjectId = getProjectId(); - this.apiKey = getApiKey(); - this.appPackageName = firebaseApp.getApplicationContext().getPackageName(); - this.appVersion = getAppVersion(); - } - - @VisibleForTesting - FirebaseMlLogger( - @NonNull FirebaseApp firebaseApp, - @NonNull SharedPreferencesUtil sharedPreferencesUtil, - @NonNull DataTransportMlEventSender eventSender) { - this.firebaseApp = firebaseApp; + FirebaseOptions options, + SharedPreferencesUtil sharedPreferencesUtil, + DataTransportMlEventSender eventSender, + @Named("appPackageName") Provider appPackageName, + @Named("appVersionCode") Provider appVersionCode) { + this.firebaseOptions = options; this.sharedPreferencesUtil = sharedPreferencesUtil; this.eventSender = eventSender; this.firebaseProjectId = getProjectId(); this.apiKey = getApiKey(); - this.appPackageName = firebaseApp.getApplicationContext().getPackageName(); - this.appVersion = getAppVersion(); - } - - /** - * Get FirebaseMlLogger instance using the firebase app returned by {@link - * FirebaseApp#getInstance()} - * - * @return FirebaseMlLogger - */ - @NonNull - public static FirebaseMlLogger getInstance() { - return FirebaseApp.getInstance().get(FirebaseMlLogger.class); - } - - @NonNull - public static FirebaseMlLogger getInstance(@NonNull FirebaseApp app) { - return app.get(FirebaseMlLogger.class); + this.appPackageName = appPackageName; + this.appVersionCode = appVersionCode; } void logModelInfoRetrieverFailure(CustomModel model, ErrorCode errorCode) { @@ -256,33 +229,15 @@ private void logDownloadEvent( private SystemInfo getSystemInfo() { return SystemInfo.builder() .setFirebaseProjectId(firebaseProjectId) - .setAppId(appPackageName) - .setAppVersion(appVersion) + .setAppId(appPackageName.get()) + .setAppVersion(appVersionCode.get()) .setApiKey(apiKey) .setMlSdkVersion(BuildConfig.VERSION_NAME) .build(); } - private String getAppVersion() { - String version = ""; - try { - PackageInfo packageInfo = - firebaseApp - .getApplicationContext() - .getPackageManager() - .getPackageInfo(firebaseApp.getApplicationContext().getPackageName(), 0); - version = String.valueOf(packageInfo.versionCode); - } catch (NameNotFoundException e) { - Log.e(TAG, "Exception thrown when trying to get app version " + e); - } - return version; - } - private String getProjectId() { - if (firebaseApp == null) { - return ""; - } - String projectId = firebaseApp.getOptions().getProjectId(); + String projectId = firebaseOptions.getProjectId(); if (projectId == null) { return ""; } @@ -290,10 +245,6 @@ private String getProjectId() { } private String getApiKey() { - if (firebaseApp == null) { - return ""; - } - String key = firebaseApp.getOptions().getApiKey(); - return key == null ? "" : key; + return firebaseOptions.getApiKey(); } } diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadService.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadService.java index ba5ae50043d..2af5578df46 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadService.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadService.java @@ -36,7 +36,6 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; import com.google.firebase.ml.modeldownloader.CustomModel; import com.google.firebase.ml.modeldownloader.CustomModelDownloadConditions; import com.google.firebase.ml.modeldownloader.FirebaseMlException; @@ -47,6 +46,7 @@ import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.inject.Inject; /** * Calls the Android Download service to copy the model file to device (temp location) and then @@ -65,6 +65,7 @@ public class ModelFileDownloadService { private final ModelFileManager fileManager; private final SharedPreferencesUtil sharedPreferencesUtil; private final FirebaseMlLogger eventLogger; + private final CustomModel.Factory modelFactory; private boolean isInitialLoad; @@ -82,40 +83,39 @@ public class ModelFileDownloadService { private CustomModelDownloadConditions downloadConditions = new CustomModelDownloadConditions.Builder().build(); - public ModelFileDownloadService(@NonNull FirebaseApp firebaseApp) { - this.context = firebaseApp.getApplicationContext(); - downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - this.fileManager = ModelFileManager.getInstance(firebaseApp); - this.sharedPreferencesUtil = new SharedPreferencesUtil(firebaseApp); - this.isInitialLoad = true; - this.eventLogger = FirebaseMlLogger.getInstance(); + @Inject + public ModelFileDownloadService( + Context context, + FirebaseMlLogger eventLogger, + ModelFileManager modelFileManager, + SharedPreferencesUtil sharedPreferencesUtil, + CustomModel.Factory modelFactory) { + this( + context, + (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE), + modelFileManager, + sharedPreferencesUtil, + eventLogger, + true, + modelFactory); } @VisibleForTesting ModelFileDownloadService( - @NonNull FirebaseApp firebaseApp, + Context context, DownloadManager downloadManager, ModelFileManager fileManager, SharedPreferencesUtil sharedPreferencesUtil, FirebaseMlLogger eventLogger, - boolean isInitialLoad) { - this.context = firebaseApp.getApplicationContext(); + boolean isInitialLoad, + CustomModel.Factory modelFactory) { + this.context = context; this.downloadManager = downloadManager; this.fileManager = fileManager; this.sharedPreferencesUtil = sharedPreferencesUtil; this.eventLogger = eventLogger; this.isInitialLoad = isInitialLoad; - } - - /** - * Get ModelFileDownloadService instance using the firebase app returned by {@link - * FirebaseApp#getInstance()} - * - * @return ModelFileDownloadService - */ - @NonNull - public static ModelFileDownloadService getInstance() { - return FirebaseApp.getInstance().get(ModelFileDownloadService.class); + this.modelFactory = modelFactory; } public Task download( @@ -291,7 +291,7 @@ synchronized Long scheduleModelDownload(@NonNull CustomModel customModel) // update the custom model to store the download id - do not lose current local file - in case // this is a background update. CustomModel model = - new CustomModel( + modelFactory.create( customModel.getName(), customModel.getModelHash(), customModel.getSize(), @@ -429,7 +429,7 @@ public File loadNewlyDownloadedModelFile(CustomModel model) { + newModelFile.getParent()); // Successfully moved, update share preferences sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel( + modelFactory.create( model.getName(), model.getModelHash(), model.getSize(), 0, newModelFile.getPath())); maybeCleanUpOldModels(); diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManager.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManager.java index aa349ad9819..b318cdc5564 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManager.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManager.java @@ -23,48 +23,39 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import com.google.firebase.FirebaseApp; import com.google.firebase.ml.modeldownloader.CustomModel; import com.google.firebase.ml.modeldownloader.FirebaseMlException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; /** * Model File Manager is used to move the downloaded file to the appropriate locations. * * @hide */ +@Singleton public class ModelFileManager { public static final String CUSTOM_MODEL_ROOT_PATH = "com.google.firebase.ml.custom.models"; private static final String TAG = "FirebaseModelFileManage"; private static final int INVALID_INDEX = -1; private final Context context; - private final FirebaseApp firebaseApp; + private final String persistenceKey; private final SharedPreferencesUtil sharedPreferencesUtil; - public ModelFileManager(@NonNull FirebaseApp firebaseApp) { - this.context = firebaseApp.getApplicationContext(); - this.firebaseApp = firebaseApp; - this.sharedPreferencesUtil = new SharedPreferencesUtil(firebaseApp); - } - - /** - * Get ModelFileDownloadService instance using the firebase app returned by {@link - * FirebaseApp#getInstance()} - * - * @return ModelFileDownloadService - */ - @NonNull - public static ModelFileManager getInstance() { - return FirebaseApp.getInstance().get(ModelFileManager.class); - } - - @NonNull - public static ModelFileManager getInstance(@NonNull FirebaseApp app) { - return app.get(ModelFileManager.class); + @Inject + public ModelFileManager( + Context applicationContext, + @Named("persistenceKey") String persistenceKey, + SharedPreferencesUtil sharedPreferencesUtil) { + this.context = applicationContext; + this.persistenceKey = persistenceKey; + this.sharedPreferencesUtil = sharedPreferencesUtil; } void deleteNonLatestCustomModels() throws FirebaseMlException { @@ -97,7 +88,7 @@ private File getModelDirUnsafe(@NonNull String modelName) { } else { root = context.getApplicationContext().getDir(modelTypeSpecificRoot, Context.MODE_PRIVATE); } - File firebaseAppDir = new File(root, firebaseApp.getPersistenceKey()); + File firebaseAppDir = new File(root, persistenceKey); return new File(firebaseAppDir, modelName); } diff --git a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtil.java b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtil.java index 4559fbcb165..e5af8131291 100644 --- a/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtil.java +++ b/firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtil.java @@ -29,8 +29,11 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.inject.Inject; +import javax.inject.Singleton; /** @hide */ +@Singleton public class SharedPreferencesUtil { public static final String FIREBASE_MODELDOWNLOADER_COLLECTION_ENABLED = @@ -58,10 +61,13 @@ public class SharedPreferencesUtil { private final String persistenceKey; private final FirebaseApp firebaseApp; + private final CustomModel.Factory modelFactory; - public SharedPreferencesUtil(FirebaseApp firebaseApp) { + @Inject + public SharedPreferencesUtil(FirebaseApp firebaseApp, CustomModel.Factory modelFactory) { this.firebaseApp = firebaseApp; this.persistenceKey = firebaseApp.getPersistenceKey(); + this.modelFactory = modelFactory; } /** @@ -99,7 +105,7 @@ public synchronized CustomModel getCustomModelDetails(@NonNull String modelName) getSharedPreferences() .getLong(String.format(DOWNLOADING_MODEL_ID_PATTERN, persistenceKey, modelName), 0); - return new CustomModel(modelName, modelHash, fileSize, id, filePath); + return modelFactory.create(modelName, modelHash, fileSize, id, filePath); } /** @@ -130,7 +136,7 @@ public synchronized CustomModel getDownloadingCustomModelDetails(@NonNull String getSharedPreferences() .getLong(String.format(DOWNLOADING_MODEL_ID_PATTERN, persistenceKey, modelName), 0); - return new CustomModel(modelName, modelHash, fileSize, id); + return modelFactory.create(modelName, modelHash, fileSize, id); } /** diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/CustomModelTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/CustomModelTest.java index c10b6ec9d75..d4abab80f06 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/CustomModelTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/CustomModelTest.java @@ -19,12 +19,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import androidx.test.core.app.ApplicationProvider; -import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService; @@ -34,8 +35,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -52,31 +51,42 @@ public class CustomModelTest { .build(); private static final long URL_EXPIRATION = 604800L; - private final CustomModel CUSTOM_MODEL = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0); - private final CustomModel CUSTOM_MODEL_URL = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION); - private final CustomModel CUSTOM_MODEL_BADFILE = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, "tmp/some/bad/filepath/model.tflite"); + private CustomModel CUSTOM_MODEL; + private CustomModel CUSTOM_MODEL_URL; + private CustomModel CUSTOM_MODEL_BADFILE; private File testModelFile; private File testModelFile2; private CustomModel customModelWithFile; - @Mock private ModelFileDownloadService fileDownloadService; + private final ModelFileDownloadService fileDownloadService = mock(ModelFileDownloadService.class); + + private final CustomModel.Factory modelFactory = + (name, modelHash, fileSize, downloadId, localFilePath, downloadUrl, downloadUrlExpiry) -> + new CustomModel( + fileDownloadService, + name, + modelHash, + fileSize, + downloadId, + localFilePath, + downloadUrl, + downloadUrlExpiry); @Before public void setUp() throws IOException { - MockitoAnnotations.initMocks(this); - FirebaseApp.clearInstancesForTest(); - // default app - FirebaseApp app = - FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS); - setUpTestingFiles(app); - customModelWithFile = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, testModelFile.getPath()); + CUSTOM_MODEL = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0); + CUSTOM_MODEL_URL = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION); + CUSTOM_MODEL_BADFILE = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, "tmp/some/bad/filepath/model.tflite"); + + setUpTestingFiles(ApplicationProvider.getApplicationContext()); + customModelWithFile = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, testModelFile.getPath()); } - private void setUpTestingFiles(FirebaseApp app) throws IOException { - final File testDir = new File(app.getApplicationContext().getNoBackupFilesDir(), "tmpModels"); + private void setUpTestingFiles(Context context) throws IOException { + final File testDir = new File(context.getNoBackupFilesDir(), "tmpModels"); testDir.mkdirs(); // make sure the directory is empty. Doesn't recurse into subdirs, but that's OK since // we're only using this directory for this test and we won't create any subdirs. @@ -168,36 +178,40 @@ public void customModel_getDownloadUrlExpiry() { @Test public void customModel_equals() { // downloading models - assertEquals(CUSTOM_MODEL, new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0)); - assertNotEquals(CUSTOM_MODEL, new CustomModel(MODEL_NAME, MODEL_HASH, 101, 0)); - assertNotEquals(CUSTOM_MODEL, new CustomModel(MODEL_NAME, MODEL_HASH, 100, 101)); + assertEquals(CUSTOM_MODEL, modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0)); + assertNotEquals(CUSTOM_MODEL, modelFactory.create(MODEL_NAME, MODEL_HASH, 101, 0)); + assertNotEquals(CUSTOM_MODEL, modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 101)); // get model details models assertEquals( - CUSTOM_MODEL_URL, new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION)); + CUSTOM_MODEL_URL, + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION)); assertNotEquals( - CUSTOM_MODEL_URL, new CustomModel(MODEL_NAME, MODEL_HASH, 101, MODEL_URL, URL_EXPIRATION)); + CUSTOM_MODEL_URL, + modelFactory.create(MODEL_NAME, MODEL_HASH, 101, MODEL_URL, URL_EXPIRATION)); assertNotEquals( CUSTOM_MODEL_URL, - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L)); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L)); } @Test public void customModel_hashCode() { assertEquals( - CUSTOM_MODEL.hashCode(), new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0).hashCode()); + CUSTOM_MODEL.hashCode(), modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0).hashCode()); assertNotEquals( - CUSTOM_MODEL.hashCode(), new CustomModel(MODEL_NAME, MODEL_HASH, 101, 0).hashCode()); + CUSTOM_MODEL.hashCode(), modelFactory.create(MODEL_NAME, MODEL_HASH, 101, 0).hashCode()); assertNotEquals( - CUSTOM_MODEL.hashCode(), new CustomModel(MODEL_NAME, MODEL_HASH, 100, 101).hashCode()); + CUSTOM_MODEL.hashCode(), modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 101).hashCode()); assertEquals( CUSTOM_MODEL_URL.hashCode(), - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION).hashCode()); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION).hashCode()); assertNotEquals( CUSTOM_MODEL_URL.hashCode(), - new CustomModel(MODEL_NAME, MODEL_HASH, 101, MODEL_URL, URL_EXPIRATION).hashCode()); + modelFactory.create(MODEL_NAME, MODEL_HASH, 101, MODEL_URL, URL_EXPIRATION).hashCode()); assertNotEquals( CUSTOM_MODEL_URL.hashCode(), - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L).hashCode()); + modelFactory + .create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L) + .hashCode()); } } diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderTest.java index cc76d7a0a00..00b26ca493a 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderTest.java @@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -36,6 +37,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.FirebaseOptions.Builder; +import com.google.firebase.concurrent.TestOnlyExecutors; import com.google.firebase.ml.modeldownloader.internal.CustomModelDownloadService; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.DownloadStatus; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent.ErrorCode; @@ -52,8 +54,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -78,21 +78,32 @@ public class FirebaseModelDownloaderTest { private static final CustomModelDownloadConditions DOWNLOAD_CONDITIONS = new CustomModelDownloadConditions.Builder().requireWifi().build(); - private final CustomModel CUSTOM_MODEL = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0); - private final CustomModel ORIG_CUSTOM_MODEL_URL = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L); - private final CustomModel UPDATE_CUSTOM_MODEL_URL = - new CustomModel(MODEL_NAME, UPDATE_MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L); - private final CustomModel UPDATE_IN_PROGRESS_CUSTOM_MODEL = - new CustomModel(MODEL_NAME, UPDATE_MODEL_HASH, 100, DOWNLOAD_ID); + private CustomModel CUSTOM_MODEL; + private CustomModel ORIG_CUSTOM_MODEL_URL; + private CustomModel UPDATE_CUSTOM_MODEL_URL; + private CustomModel UPDATE_IN_PROGRESS_CUSTOM_MODEL; private CustomModel customModelUpdateLoaded; private CustomModel customModelLoaded; - private @Mock SharedPreferencesUtil mockPrefs; - private @Mock ModelFileDownloadService mockFileDownloadService; - private @Mock CustomModelDownloadService mockModelDownloadService; - private @Mock ModelFileManager mockFileManager; - private @Mock FirebaseMlLogger mockEventLogger; + private final SharedPreferencesUtil mockPrefs = mock(SharedPreferencesUtil.class); + private final ModelFileDownloadService mockFileDownloadService = + mock(ModelFileDownloadService.class); + private final CustomModelDownloadService mockModelDownloadService = + mock(CustomModelDownloadService.class); + private final ModelFileManager mockFileManager = mock(ModelFileManager.class); + private final FirebaseMlLogger mockEventLogger = mock(FirebaseMlLogger.class); + + private final CustomModel.Factory modelFactory = + (name, modelHash, fileSize, downloadId, localFilePath, downloadUrl, downloadUrlExpiry) -> + new CustomModel( + mockFileDownloadService, + name, + modelHash, + fileSize, + downloadId, + localFilePath, + downloadUrl, + downloadUrlExpiry); private FirebaseModelDownloader firebaseModelDownloader; private ExecutorService executor; @@ -107,7 +118,14 @@ public class FirebaseModelDownloaderTest { @Before public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); + CUSTOM_MODEL = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0); + ORIG_CUSTOM_MODEL_URL = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L); + UPDATE_CUSTOM_MODEL_URL = + modelFactory.create(MODEL_NAME, UPDATE_MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION + 10L); + UPDATE_IN_PROGRESS_CUSTOM_MODEL = + modelFactory.create(MODEL_NAME, UPDATE_MODEL_HASH, 100, DOWNLOAD_ID); + FirebaseApp.clearInstancesForTest(); FirebaseApp app = FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS); @@ -120,7 +138,9 @@ public void setUp() throws Exception { mockModelDownloadService, mockFileManager, mockEventLogger, - executor); + TestOnlyExecutors.background(), + TestOnlyExecutors.blocking(), + modelFactory); setUpTestingFiles(app); doNothing().when(mockEventLogger).logDownloadEventWithExactDownloadTime(any(), any(), any()); @@ -131,7 +151,11 @@ public void setUp() throws Exception { } private void setUpTestingFiles(FirebaseApp app) throws Exception { - fileManager = new ModelFileManager(app); + fileManager = + new ModelFileManager( + app.getApplicationContext(), + app.getPersistenceKey(), + new SharedPreferencesUtil(app, modelFactory)); final File testDir = new File(app.getApplicationContext().getNoBackupFilesDir(), "tmpModels"); testDir.mkdirs(); // make sure the directory is empty. Doesn't recurse into subdirs, but that's OK since @@ -172,9 +196,9 @@ private void setUpTestingFiles(FirebaseApp app) throws Exception { fd2.close(); customModelLoaded = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, expectedDestinationFolder + "/0"); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, expectedDestinationFolder + "/0"); customModelUpdateLoaded = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, expectedDestinationFolder + "/1"); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, expectedDestinationFolder + "/1"); } @After @@ -210,7 +234,8 @@ public void getModel_latestModel_localExists_noUpdate() throws Exception { public void getModel_latestModel_localExists_noUpdate_MissingFile() throws Exception { // model with missing file. CustomModel missingFileModel = - new CustomModel(MODEL_NAME, UPDATE_MODEL_HASH, 100, 0, expectedDestinationFolder + "/4"); + modelFactory.create( + MODEL_NAME, UPDATE_MODEL_HASH, 100, 0, expectedDestinationFolder + "/4"); when(mockPrefs.getCustomModelDetails(eq(MODEL_NAME))) .thenReturn(missingFileModel) .thenReturn(customModelUpdateLoaded); @@ -240,7 +265,7 @@ public void getModel_latestModel_localExists_noUpdate_MissingFile() throws Excep @Test public void getModel_latestModel_localExists_noUpdate_MissingDownloadId() throws Exception { - CustomModel badLocalModel = new CustomModel(MODEL_NAME, UPDATE_MODEL_HASH, 100, 0); + CustomModel badLocalModel = modelFactory.create(MODEL_NAME, UPDATE_MODEL_HASH, 100, 0); when(mockPrefs.getCustomModelDetails(eq(MODEL_NAME))) .thenReturn(badLocalModel) // getlocalModelDetails 1 .thenReturn(null) // getCustomModelTask 1 @@ -275,7 +300,7 @@ public void getModel_latestModel_localExists_noUpdate_MissingDownloadId() throws @Test public void getModel_latestModel_localExists_noUpdate_inProgress() throws Exception { // model with no file yet. - CustomModel inProgressLocalModel = new CustomModel(MODEL_NAME, UPDATE_MODEL_HASH, 100, 88); + CustomModel inProgressLocalModel = modelFactory.create(MODEL_NAME, UPDATE_MODEL_HASH, 100, 88); when(mockPrefs.getCustomModelDetails(eq(MODEL_NAME))) .thenReturn(inProgressLocalModel) // getlocalModelDetails 1 .thenReturn(inProgressLocalModel) // getCustomModelTask 1 @@ -356,7 +381,7 @@ public void getModel_latestModel_localExists_UpdateFound() throws Exception { @Test public void getModel_latestModel_localExists_DownloadInProgress() throws Exception { CustomModel customModelLoadedWithDownload = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 99, expectedDestinationFolder + "/0"); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 99, expectedDestinationFolder + "/0"); when(mockPrefs.getCustomModelDetails(eq(MODEL_NAME))).thenReturn(customModelLoadedWithDownload); when(mockPrefs.getDownloadingCustomModelDetails(eq(MODEL_NAME))) @@ -423,7 +448,6 @@ public void getModel_latestModel_noLocalModel_modelDownloadServiceFails() throws } verify(mockPrefs, times(2)).getCustomModelDetails(eq(MODEL_NAME)); - verify(mockFileDownloadService, never()).loadNewlyDownloadedModelFile(any()); assertThat(task.isComplete()).isTrue(); assertThat(task.isSuccessful()).isFalse(); assertTrue(task.getException().getMessage().contains("bad state")); diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadServiceTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadServiceTest.java index 12da8853d2d..6f5a3634b9b 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadServiceTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/CustomModelDownloadServiceTest.java @@ -121,6 +121,20 @@ public Builder toBuilder() { private FirebaseInstallationsApi installationsApiMock; @Mock private FirebaseMlLogger mockEventLogger; + private final ModelFileDownloadService modelFileDownloadService = + mock(ModelFileDownloadService.class); + private final CustomModel.Factory modelFactory = + (name, modelHash, fileSize, downloadId, localFilePath, downloadUrl, downloadUrlExpiry) -> + new CustomModel( + modelFileDownloadService, + name, + modelHash, + fileSize, + downloadId, + localFilePath, + downloadUrl, + downloadUrlExpiry); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -172,18 +186,20 @@ public void downloadService_noHashSuccess() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getNewDownloadUrlWithExpiry(PROJECT_ID, MODEL_NAME); Assert.assertEquals( modelTask.getResult(), - new CustomModel(MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); + modelFactory.create( + MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); WireMock.verify( getRequestedFor(urlEqualTo(downloadPath)) @@ -220,18 +236,20 @@ public void downloadService_fingerPrintHashNull_NoCertHeader() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, null, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getNewDownloadUrlWithExpiry(PROJECT_ID, MODEL_NAME); Assert.assertEquals( modelTask.getResult(), - new CustomModel(MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); + modelFactory.create( + MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); WireMock.verify( getRequestedFor(urlEqualTo(downloadPath)) @@ -266,18 +284,20 @@ public void downloadService_withHashSuccess_noMatch() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); Assert.assertEquals( modelTask.getResult(), - new CustomModel(MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); + modelFactory.create( + MODEL_NAME, MODEL_HASH, FILE_SIZE, DOWNLOAD_URI, TEST_EXPIRATION_IN_MS)); WireMock.verify( getRequestedFor(urlEqualTo(downloadPath)) @@ -314,12 +334,13 @@ public void downloadService_withHashSuccess_match() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -363,12 +384,13 @@ public void downloadService_modelNotFound() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -415,12 +437,13 @@ public void downloadService_badRequest() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -473,12 +496,13 @@ public void downloadService_forbidden() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -531,12 +555,13 @@ public void downloadService_internalError() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -588,12 +613,13 @@ public void downloadService_tooManyRequest() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -643,12 +669,13 @@ public void downloadService_authenticationIssue() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -690,12 +717,13 @@ public void downloadService_unauthenticatedToken() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -718,12 +746,13 @@ public void downloadService_nullModelHashPassedUnauthenticatedToken() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, null); @@ -747,12 +776,13 @@ public void downloadService_malFormedUrl() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, "https7://localhost:8989/barUrl", - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); @@ -775,12 +805,13 @@ public void downloadService_unauthenticatedToken_noNetworkConnection() { CustomModelDownloadService service = new CustomModelDownloadService( ApplicationProvider.getApplicationContext(), - installationsApiMock, + () -> installationsApiMock, directExecutor, API_KEY, PACKAGE_FINGERPRINT_HASH, TEST_ENDPOINT, - mockEventLogger); + mockEventLogger, + modelFactory); Task modelTask = service.getCustomModelDetails(PROJECT_ID, MODEL_NAME, MODEL_HASH); diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSenderTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSenderTest.java index 4ca7f441664..576280c8c87 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSenderTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/DataTransportMlEventSenderTest.java @@ -18,13 +18,16 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.android.datatransport.Transport; +import com.google.android.datatransport.TransportFactory; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.EventName; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.SystemInfo; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -32,6 +35,7 @@ @RunWith(RobolectricTestRunner.class) public class DataTransportMlEventSenderTest { + @Mock private TransportFactory mockTransportFactory; @Mock private Transport mockTransport; private DataTransportMlEventSender statsSender; @@ -48,7 +52,10 @@ public class DataTransportMlEventSenderTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - statsSender = new DataTransportMlEventSender(mockTransport); + when(mockTransportFactory.getTransport( + any(), ArgumentMatchers.>any(), any(), any())) + .thenReturn(mockTransport); + statsSender = new DataTransportMlEventSender(() -> mockTransportFactory); } @Test diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLoggerTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLoggerTest.java index 7903278ccfe..501f17df49a 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLoggerTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/FirebaseMlLoggerTest.java @@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; @@ -33,6 +34,7 @@ import com.google.firebase.ml.modeldownloader.BuildConfig; import com.google.firebase.ml.modeldownloader.CustomModel; import com.google.firebase.ml.modeldownloader.FirebaseMlException; +import com.google.firebase.ml.modeldownloader.FirebaseModelDownloader; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.DeleteModelLogEvent; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.EventName; import com.google.firebase.ml.modeldownloader.internal.FirebaseMlLogEvent.ModelDownloadLogEvent; @@ -45,9 +47,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) @@ -69,26 +69,42 @@ public class FirebaseMlLoggerTest { private static final String MODEL_HASH = "dsf324"; private static final long SYSTEM_TIME = 2000; private static final Long DOWNLOAD_ID = 987923L; - private static final CustomModel CUSTOM_MODEL_DOWNLOADING = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, DOWNLOAD_ID); + private CustomModel CUSTOM_MODEL_DOWNLOADING; private static final ModelOptions MODEL_OPTIONS = ModelOptions.builder() .setModelInfo(ModelInfo.builder().setName(MODEL_NAME).setHash(MODEL_HASH).build()) .build(); - @Mock private SharedPreferencesUtil mockSharedPreferencesUtil; - @Mock private DataTransportMlEventSender mockStatsSender; + private final SharedPreferencesUtil mockSharedPreferencesUtil = mock(SharedPreferencesUtil.class); + private final DataTransportMlEventSender mockStatsSender = mock(DataTransportMlEventSender.class); private FirebaseMlLogger mlLogger; + private CustomModel.Factory modelFactory; @Before public void setUp() throws NameNotFoundException { - MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); FirebaseApp app = FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS); - mlLogger = new FirebaseMlLogger(app, mockSharedPreferencesUtil, mockStatsSender); + modelFactory = FirebaseModelDownloader.getInstance(app).getModelFactory(); + mlLogger = + new FirebaseMlLogger( + FIREBASE_OPTIONS, + mockSharedPreferencesUtil, + mockStatsSender, + () -> ApplicationProvider.getApplicationContext().getPackageName(), + () -> { + try { + return String.valueOf( + app.getApplicationContext() + .getPackageManager() + .getPackageInfo(app.getApplicationContext().getPackageName(), 0) + .versionCode); + } catch (NameNotFoundException e) { + return ""; + } + }); systemInfo = SystemInfo.builder() .setFirebaseProjectId(TEST_PROJECT_ID) @@ -109,6 +125,7 @@ public void setUp() throws NameNotFoundException { doNothing().when(mockSharedPreferencesUtil).setModelDownloadCompleteTimeMs(any(), anyLong()); when(mockSharedPreferencesUtil.getCustomModelStatsCollectionFlag()).thenReturn(true); SystemClock.setCurrentTimeMillis(SYSTEM_TIME + 500); + CUSTOM_MODEL_DOWNLOADING = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, DOWNLOAD_ID); } @Test diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadServiceTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadServiceTest.java index 90e6eed211c..6f3c186bdfd 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadServiceTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadServiceTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -54,12 +55,11 @@ import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.LooperMode; @@ -83,16 +83,11 @@ public class ModelFileDownloadServiceTest { private static final long URL_EXPIRATION_FUTURE = (new Date()).getTime() + 600000; private static final Long DOWNLOAD_ID = 987923L; - private static final CustomModel CUSTOM_MODEL_PREVIOUS_LOADED = - new CustomModel(MODEL_NAME, MODEL_HASH + "2", 105, 0, "FakeFile/path.tflite"); - private static final CustomModel CUSTOM_MODEL_NO_URL = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0); - private static final CustomModel CUSTOM_MODEL_URL = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION_FUTURE); - private static final CustomModel CUSTOM_MODEL_EXPIRED_URL = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION_OLD); - private static final CustomModel CUSTOM_MODEL_DOWNLOADING = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, DOWNLOAD_ID); + private CustomModel CUSTOM_MODEL_PREVIOUS_LOADED; + private CustomModel CUSTOM_MODEL_NO_URL; + private CustomModel CUSTOM_MODEL_URL; + private CustomModel CUSTOM_MODEL_EXPIRED_URL; + private CustomModel CUSTOM_MODEL_DOWNLOADING; CustomModel customModelDownloadComplete; private static final CustomModelDownloadConditions DOWNLOAD_CONDITIONS_CHARGING_IDLE = @@ -101,52 +96,77 @@ public class ModelFileDownloadServiceTest { File testTempModelFile; File testAppModelFile; + private final DownloadManager mockDownloadManager = mock(DownloadManager.class); + private final ModelFileManager mockFileManager = mock(ModelFileManager.class); + private final FirebaseMlLogger mockStatsLogger = mock(FirebaseMlLogger.class); + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private ModelFileDownloadService modelFileDownloadService; private ModelFileDownloadService modelFileDownloadServiceInitialLoad; private SharedPreferencesUtil sharedPreferencesUtil; - @Mock DownloadManager mockDownloadManager; - @Mock ModelFileManager mockFileManager; - @Mock FirebaseMlLogger mockStatsLogger; + private CustomModel.Factory modelFactory; - ExecutorService executor; private MatrixCursor matrixCursor; FirebaseApp app; @Before public void setUp() throws IOException { - MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); app = FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS); - executor = Executors.newSingleThreadExecutor(); - sharedPreferencesUtil = new SharedPreferencesUtil(app); + AtomicReference serviceRef = new AtomicReference<>(); + modelFactory = + (name, modelHash, fileSize, downloadId, localFilePath, downloadUrl, downloadUrlExpiry) -> + new CustomModel( + serviceRef.get(), + name, + modelHash, + fileSize, + downloadId, + localFilePath, + downloadUrl, + downloadUrlExpiry); + sharedPreferencesUtil = new SharedPreferencesUtil(app, modelFactory); sharedPreferencesUtil.clearModelDetails(MODEL_NAME); modelFileDownloadService = new ModelFileDownloadService( - app, + ApplicationProvider.getApplicationContext(), mockDownloadManager, mockFileManager, sharedPreferencesUtil, mockStatsLogger, - false); + false, + modelFactory); + serviceRef.set(modelFileDownloadService); modelFileDownloadServiceInitialLoad = new ModelFileDownloadService( - app, + ApplicationProvider.getApplicationContext(), mockDownloadManager, mockFileManager, sharedPreferencesUtil, mockStatsLogger, - true); + true, + modelFactory); + + CUSTOM_MODEL_PREVIOUS_LOADED = + modelFactory.create(MODEL_NAME, MODEL_HASH + "2", 105, 0, "FakeFile/path.tflite"); + CUSTOM_MODEL_NO_URL = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0); + CUSTOM_MODEL_URL = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION_FUTURE); + CUSTOM_MODEL_EXPIRED_URL = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION_OLD); + CUSTOM_MODEL_DOWNLOADING = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, DOWNLOAD_ID); matrixCursor = new MatrixCursor(new String[] {DownloadManager.COLUMN_STATUS}); testTempModelFile = File.createTempFile("fakeTempFile", ".tflite"); testAppModelFile = File.createTempFile("fakeAppFile", ".tflite"); customModelDownloadComplete = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, testAppModelFile.getPath()); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, testAppModelFile.getPath()); } @After @@ -412,7 +432,7 @@ public void ensureModelDownloaded_downloadFailed_urlExpiry() { when(mockDownloadManager.query(any())).thenReturn(matrixCursor); CustomModel justAboutToExpireModel = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, (new Date()).getTime() + 3); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, (new Date()).getTime() + 3); TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); Task task = modelFileDownloadService.ensureModelDownloaded(justAboutToExpireModel); @@ -611,7 +631,7 @@ public void ensureModelDownloaded_alreadyInProgess_UrlExpired() throws Exception // set up the first request CustomModel justAboutToExpireModel = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, (new Date()).getTime() + 30); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, (new Date()).getTime() + 30); TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); Task task = modelFileDownloadService.ensureModelDownloaded(justAboutToExpireModel); task.addOnCompleteListener(executor, onCompleteListener); @@ -820,7 +840,8 @@ public void maybeCheckDownloadingComplete_downloadInprogress() { public void maybeCheckDownloadingComplete_multipleDownloads() throws Exception { sharedPreferencesUtil.setDownloadingCustomModelDetails(CUSTOM_MODEL_DOWNLOADING); String secondModelName = "secondModelName"; - CustomModel downloading2 = new CustomModel(secondModelName, MODEL_HASH, 100, DOWNLOAD_ID + 1); + CustomModel downloading2 = + modelFactory.create(secondModelName, MODEL_HASH, 100, DOWNLOAD_ID + 1); sharedPreferencesUtil.setDownloadingCustomModelDetails(downloading2); assertNull(modelFileDownloadService.getDownloadingModelStatusCode(0L)); @@ -839,7 +860,7 @@ public void maybeCheckDownloadingComplete_multipleDownloads() throws Exception { sharedPreferencesUtil.getCustomModelDetails(MODEL_NAME), customModelDownloadComplete); assertEquals( sharedPreferencesUtil.getCustomModelDetails(secondModelName), - new CustomModel(secondModelName, MODEL_HASH, 100, 0, testAppModelFile.getPath())); + modelFactory.create(secondModelName, MODEL_HASH, 100, 0, testAppModelFile.getPath())); verify(mockDownloadManager, times(5)).query(any()); verify(mockDownloadManager, times(2)).remove(anyLong()); } diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManagerTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManagerTest.java index 8dac9c6dc4c..0c70673a63c 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManagerTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManagerTest.java @@ -25,6 +25,7 @@ import com.google.firebase.FirebaseOptions.Builder; import com.google.firebase.ml.modeldownloader.CustomModel; import com.google.firebase.ml.modeldownloader.FirebaseMlException; +import com.google.firebase.ml.modeldownloader.FirebaseModelDownloader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -51,8 +52,8 @@ public class ModelFileManagerTest { public static final String MODEL_NAME_2 = "MODEL_NAME_2"; public static final String MODEL_HASH_2 = "hash2"; - final CustomModel CUSTOM_MODEL_NO_FILE = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0); - final CustomModel CUSTOM_MODEL_NO_FILE_2 = new CustomModel(MODEL_NAME_2, MODEL_HASH_2, 101, 0); + private CustomModel CUSTOM_MODEL_NO_FILE; + private CustomModel CUSTOM_MODEL_NO_FILE_2; private File testModelFile; private File testModelFile2; @@ -60,7 +61,8 @@ public class ModelFileManagerTest { ModelFileManager fileManager; FirebaseApp app; private SharedPreferencesUtil sharedPreferencesUtil; - String modelDestinationFolder; + private String modelDestinationFolder; + private CustomModel.Factory modelFactory; @Before public void setUp() throws IOException { @@ -68,10 +70,18 @@ public void setUp() throws IOException { FirebaseApp.clearInstancesForTest(); app = FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS); - sharedPreferencesUtil = new SharedPreferencesUtil(app); - fileManager = new ModelFileManager(app); + modelFactory = FirebaseModelDownloader.getInstance(app).getModelFactory(); + + sharedPreferencesUtil = new SharedPreferencesUtil(app, modelFactory); + fileManager = + new ModelFileManager( + ApplicationProvider.getApplicationContext(), + app.getPersistenceKey(), + sharedPreferencesUtil); modelDestinationFolder = setUpTestingFiles(app, MODEL_NAME); + CUSTOM_MODEL_NO_FILE = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0); + CUSTOM_MODEL_NO_FILE_2 = modelFactory.create(MODEL_NAME_2, MODEL_HASH_2, 101, 0); } private String setUpTestingFiles(FirebaseApp app, String modelName) throws IOException { @@ -181,7 +191,7 @@ public void deleteNonLatestCustomModels_fileToDelete() MoveFileToDestination(modelDestinationFolder, testModelFile2, CUSTOM_MODEL_NO_FILE, 1); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); fileManager.deleteNonLatestCustomModels(); assertFalse(new File(modelDestinationFolder + "/0").exists()); @@ -199,11 +209,11 @@ public void deleteNonLatestCustomModels_whenModelOnDiskButNotInPreferences() MoveFileToDestination(modelDestinationFolder2, testModelFile2, CUSTOM_MODEL_NO_FILE_2, 0); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/0")); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/0")); // Download in progress, hence file path is not present sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME_2, MODEL_HASH_2, 100, 0)); + modelFactory.create(MODEL_NAME_2, MODEL_HASH_2, 100, 0)); fileManager.deleteNonLatestCustomModels(); @@ -217,7 +227,7 @@ public void deleteNonLatestCustomModels_noFileToDelete() MoveFileToDestination(modelDestinationFolder, testModelFile, CUSTOM_MODEL_NO_FILE, 0); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/0")); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/0")); fileManager.deleteNonLatestCustomModels(); assertTrue(new File(modelDestinationFolder + "/0").exists()); @@ -230,14 +240,14 @@ public void deleteNonLatestCustomModels_multipleNamedModels() MoveFileToDestination(modelDestinationFolder, testModelFile2, CUSTOM_MODEL_NO_FILE, 1); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); String modelDestinationFolder2 = setUpTestingFiles(app, MODEL_NAME_2); MoveFileToDestination(modelDestinationFolder2, testModelFile, CUSTOM_MODEL_NO_FILE_2, 0); MoveFileToDestination(modelDestinationFolder2, testModelFile2, CUSTOM_MODEL_NO_FILE_2, 1); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME_2, MODEL_HASH_2, 101, 0, modelDestinationFolder2 + "/1")); + modelFactory.create(MODEL_NAME_2, MODEL_HASH_2, 101, 0, modelDestinationFolder2 + "/1")); fileManager.deleteNonLatestCustomModels(); @@ -297,14 +307,14 @@ public void deleteOldModels_multipleNamedModels() throws FirebaseMlException, IO MoveFileToDestination(modelDestinationFolder, testModelFile2, CUSTOM_MODEL_NO_FILE, 1); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, modelDestinationFolder + "/1")); String modelDestinationFolder2 = setUpTestingFiles(app, MODEL_NAME_2); MoveFileToDestination(modelDestinationFolder2, testModelFile, CUSTOM_MODEL_NO_FILE_2, 0); MoveFileToDestination(modelDestinationFolder2, testModelFile2, CUSTOM_MODEL_NO_FILE_2, 1); sharedPreferencesUtil.setLoadedCustomModelDetails( - new CustomModel(MODEL_NAME_2, MODEL_HASH_2, 101, 0, modelDestinationFolder2 + "/1")); + modelFactory.create(MODEL_NAME_2, MODEL_HASH_2, 101, 0, modelDestinationFolder2 + "/1")); fileManager.deleteOldModels(MODEL_NAME, modelDestinationFolder + "/1"); diff --git a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtilTest.java b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtilTest.java index 5bd1722b603..22c4fb442f4 100644 --- a/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtilTest.java +++ b/firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtilTest.java @@ -25,6 +25,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ml.modeldownloader.CustomModel; +import com.google.firebase.ml.modeldownloader.FirebaseModelDownloader; import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -39,14 +40,12 @@ public class SharedPreferencesUtilTest { private static final String TEST_PROJECT_ID = "777777777777"; private static final String MODEL_NAME = "ModelName"; private static final String MODEL_HASH = "dsf324"; - private static final CustomModel CUSTOM_MODEL_DOWNLOAD_COMPLETE = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, "file/path/store/ModelName/1"); - private static final CustomModel CUSTOM_MODEL_UPDATE_IN_BACKGROUND = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 986, "file/path/store/ModelName/1"); - private static final CustomModel CUSTOM_MODEL_DOWNLOADING = - new CustomModel(MODEL_NAME, MODEL_HASH, 100, 986); + private CustomModel CUSTOM_MODEL_DOWNLOAD_COMPLETE; + private CustomModel CUSTOM_MODEL_UPDATE_IN_BACKGROUND; + private CustomModel CUSTOM_MODEL_DOWNLOADING; private SharedPreferencesUtil sharedPreferencesUtil; private FirebaseApp app; + private CustomModel.Factory modelFactory; @Before public void setUp() { @@ -60,10 +59,17 @@ public void setUp() { .setProjectId(TEST_PROJECT_ID) .build()); + modelFactory = FirebaseModelDownloader.getInstance(app).getModelFactory(); + app.setDataCollectionDefaultEnabled(Boolean.TRUE); // default sharedPreferenceUtil - sharedPreferencesUtil = new SharedPreferencesUtil(app); + sharedPreferencesUtil = new SharedPreferencesUtil(app, modelFactory); assertNotNull(sharedPreferencesUtil); + CUSTOM_MODEL_DOWNLOAD_COMPLETE = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 0, "file/path/store/ModelName/1"); + CUSTOM_MODEL_UPDATE_IN_BACKGROUND = + modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 986, "file/path/store/ModelName/1"); + CUSTOM_MODEL_DOWNLOADING = modelFactory.create(MODEL_NAME, MODEL_HASH, 100, 986); } @Test @@ -158,11 +164,13 @@ public void listDownloadedModels_multipleModels() { sharedPreferencesUtil.setLoadedCustomModelDetails(CUSTOM_MODEL_DOWNLOAD_COMPLETE); CustomModel model2 = - new CustomModel(MODEL_NAME + "2", MODEL_HASH + "2", 102, 0, "file/path/store/ModelName2/1"); + modelFactory.create( + MODEL_NAME + "2", MODEL_HASH + "2", 102, 0, "file/path/store/ModelName2/1"); sharedPreferencesUtil.setLoadedCustomModelDetails(model2); CustomModel model3 = - new CustomModel(MODEL_NAME + "3", MODEL_HASH + "3", 103, 0, "file/path/store/ModelName3/1"); + modelFactory.create( + MODEL_NAME + "3", MODEL_HASH + "3", 103, 0, "file/path/store/ModelName3/1"); sharedPreferencesUtil.setLoadedCustomModelDetails(model3); @@ -185,7 +193,7 @@ public void getCustomModelStatsCollectionFlag_defaultFirebaseAppTrue() { public void getCustomModelStatsCollectionFlag_defaultFirebaseAppFalse() { app.setDataCollectionDefaultEnabled(Boolean.FALSE); // default sharedPreferenceUtil - SharedPreferencesUtil disableLogUtil = new SharedPreferencesUtil(app); + SharedPreferencesUtil disableLogUtil = new SharedPreferencesUtil(app, modelFactory); assertEquals( disableLogUtil.getCustomModelStatsCollectionFlag(), app.isDataCollectionDefaultEnabled()); assertFalse(disableLogUtil.getCustomModelStatsCollectionFlag()); @@ -195,7 +203,7 @@ public void getCustomModelStatsCollectionFlag_defaultFirebaseAppFalse() { public void getCustomModelStatsCollectionFlag_overrideFirebaseAppFalse() { app.setDataCollectionDefaultEnabled(Boolean.FALSE); // default sharedPreferenceUtil - SharedPreferencesUtil sharedPreferencesUtil2 = new SharedPreferencesUtil(app); + SharedPreferencesUtil sharedPreferencesUtil2 = new SharedPreferencesUtil(app, modelFactory); sharedPreferencesUtil2.setCustomModelStatsCollectionEnabled(true); assertEquals(sharedPreferencesUtil2.getCustomModelStatsCollectionFlag(), true); assertTrue(sharedPreferencesUtil2.getCustomModelStatsCollectionFlag()); diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index f1da211f000..885e9681f5d 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,15 +1,18 @@ # Unreleased -* +# 20.3.0 +* [fixed] Fixed a `NullPointerException` crash when instrumenting screen + traces on Android 7, 8, and 9. + (#4146) ## Kotlin The Kotlin extensions library transitively includes the updated `firebase-performance` library. The Kotlin extensions library has the following additional updates: -* [feature] Added a [`trace(String, Trace.() -> T)`](https://firebase.google.com/docs/reference/kotlin/com/google/firebase/perf/ktx/package-summary#trace(kotlin.String,kotlin.Function1)) - extension function to create a custom trace with the given name. - +* [feature] Added a + [`trace(String, Trace.() -> T)`](/docs/reference/kotlin/com/google/firebase/perf/ktx/package-summary#trace(kotlin.String,kotlin.Function1)) + extension function to create a custom trace with the specified name. # 20.2.0 * [unchanged] Updated to accommodate the release of the updated [perfmon] Kotlin extensions library. diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index 62107170b8e..53cc557970b 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -92,6 +92,7 @@ android { dependencies { // Firebase Deps + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(':firebase-config') diff --git a/firebase-perf/gradle.properties b/firebase-perf/gradle.properties index 3d1d3baae96..66f63ef0e1e 100644 --- a/firebase-perf/gradle.properties +++ b/firebase-perf/gradle.properties @@ -15,7 +15,7 @@ # # -version=20.1.2 -latestReleasedVersion=20.1.1 +version=20.3.1 +latestReleasedVersion=20.3.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-perf/ktx/ktx.gradle b/firebase-perf/ktx/ktx.gradle index 74bbabc2308..0ce6eecd7d2 100644 --- a/firebase-perf/ktx/ktx.gradle +++ b/firebase-perf/ktx/ktx.gradle @@ -50,7 +50,7 @@ dependencies { implementation project(':firebase-perf') implementation 'androidx.annotation:annotation:1.1.0' - testCompileOnly "com.google.protobuf:protobuf-java:$protocVersion" + testCompileOnly "com.google.protobuf:protobuf-java:3.21.9" testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation 'junit:junit:4.12' testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-perf/ktx/src/main/kotlin/com/google/firebase/perf/ktx/Performance.kt b/firebase-perf/ktx/src/main/kotlin/com/google/firebase/perf/ktx/Performance.kt index 244c2d67226..fdebcf826bf 100644 --- a/firebase-perf/ktx/src/main/kotlin/com/google/firebase/perf/ktx/Performance.kt +++ b/firebase-perf/ktx/src/main/kotlin/com/google/firebase/perf/ktx/Performance.kt @@ -26,31 +26,37 @@ import com.google.firebase.platforminfo.LibraryVersionComponent /** Returns the [FirebasePerformance] instance of the default [FirebaseApp]. */ val Firebase.performance: FirebasePerformance - get() = FirebasePerformance.getInstance() + get() = FirebasePerformance.getInstance() -/** Measures the time it takes to run the [block] wrapped by calls to [start] and [stop] using [HttpMetric]. */ +/** + * Measures the time it takes to run the [block] wrapped by calls to [start] and [stop] using + * [HttpMetric]. + */ inline fun HttpMetric.trace(block: HttpMetric.() -> Unit) { - start() - try { - block() - } finally { - stop() - } + start() + try { + block() + } finally { + stop() + } } -/** Measures the time it takes to run the [block] wrapped by calls to [start] and [stop] using [Trace]. */ +/** + * Measures the time it takes to run the [block] wrapped by calls to [start] and [stop] using + * [Trace]. + */ inline fun Trace.trace(block: Trace.() -> T): T { - start() - try { - return block() - } finally { - stop() - } + start() + try { + return block() + } finally { + stop() + } } /** - * Creates a [Trace] object with given [name] and measures the time it takes to - * run the [block] wrapped by calls to [start] and [stop]. + * Creates a [Trace] object with given [name] and measures the time it takes to run the [block] + * wrapped by calls to [start] and [stop]. */ inline fun trace(name: String, block: Trace.() -> T): T = Trace.create(name).trace(block) @@ -59,6 +65,6 @@ internal const val LIBRARY_NAME: String = "fire-perf-ktx" /** @suppress */ @Keep class FirebasePerfKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/ktx/PerformanceTests.kt b/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/ktx/PerformanceTests.kt index 96d7a00557a..7278dde76ec 100644 --- a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/ktx/PerformanceTests.kt +++ b/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/ktx/PerformanceTests.kt @@ -44,9 +44,9 @@ import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.nullable import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.doAnswer import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner @@ -56,109 +56,101 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - open fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + open fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class PerformanceTests : BaseTestCase() { - @Mock - lateinit var transportManagerMock: TransportManager + @Mock lateinit var transportManagerMock: TransportManager - @Mock - lateinit var timerMock: Timer + @Mock lateinit var timerMock: Timer - @Mock - lateinit var mockTransportManager: TransportManager + @Mock lateinit var mockTransportManager: TransportManager - @Mock - lateinit var mockClock: Clock + @Mock lateinit var mockClock: Clock - @Mock - lateinit var mockAppStateMonitor: AppStateMonitor + @Mock lateinit var mockAppStateMonitor: AppStateMonitor - @Captor - lateinit var argMetricCaptor: ArgumentCaptor + @Captor lateinit var argMetricCaptor: ArgumentCaptor - @Captor - lateinit var argumentsCaptor: ArgumentCaptor + @Captor lateinit var argumentsCaptor: ArgumentCaptor - var currentTime: Long = 1 + var currentTime: Long = 1 - @Before - override fun setUp() { - super.setUp() - initMocks(this) + @Before + override fun setUp() { + super.setUp() + initMocks(this) - `when`(timerMock.getMicros()).thenReturn(1000L) - `when`(timerMock.getDurationMicros()).thenReturn(2000L).thenReturn(3000L) - doAnswer { - Timer(currentTime) - }.`when`(mockClock).getTime() - } + `when`(timerMock.getMicros()).thenReturn(1000L) + `when`(timerMock.getDurationMicros()).thenReturn(2000L).thenReturn(3000L) + doAnswer { Timer(currentTime) }.`when`(mockClock).getTime() + } - @Test - fun `performance should delegate to FirebasePerformance#getInstance()`() { - assertThat(Firebase.performance).isSameInstanceAs(FirebasePerformance.getInstance()) - } + @Test + fun `performance should delegate to FirebasePerformance#getInstance()`() { + assertThat(Firebase.performance).isSameInstanceAs(FirebasePerformance.getInstance()) + } - @Test - fun `httpMetric wrapper test `() { - val metric = HttpMetric("https://www.google.com/", HttpMethod.GET, transportManagerMock, timerMock) - metric.trace { - setHttpResponseCode(200) - } + @Test + fun `httpMetric wrapper test `() { + val metric = + HttpMetric("https://www.google.com/", HttpMethod.GET, transportManagerMock, timerMock) + metric.trace { setHttpResponseCode(200) } - verify(transportManagerMock) - .log(argMetricCaptor.capture(), ArgumentMatchers.nullable(ApplicationProcessState::class.java)) + verify(transportManagerMock) + .log( + argMetricCaptor.capture(), + ArgumentMatchers.nullable(ApplicationProcessState::class.java) + ) - val metricValue = argMetricCaptor.getValue() - assertThat(metricValue.getHttpResponseCode()).isEqualTo(200) - } + val metricValue = argMetricCaptor.getValue() + assertThat(metricValue.getHttpResponseCode()).isEqualTo(200) + } - @Test - fun `trace wrapper test`() { - val trace = Trace("trace_1", mockTransportManager, mockClock, mockAppStateMonitor) - trace.trace { - incrementMetric("metric_1", 5) - } + @Test + fun `trace wrapper test`() { + val trace = Trace("trace_1", mockTransportManager, mockClock, mockAppStateMonitor) + trace.trace { incrementMetric("metric_1", 5) } - assertThat(getTraceCounter(trace)).hasSize(1) - assertThat(getTraceCounterCount(trace, "metric_1")).isEqualTo(5) - verify(mockTransportManager).log(argumentsCaptor.capture(), nullable(ApplicationProcessState::class.java)) - } + assertThat(getTraceCounter(trace)).hasSize(1) + assertThat(getTraceCounterCount(trace, "metric_1")).isEqualTo(5) + verify(mockTransportManager) + .log(argumentsCaptor.capture(), nullable(ApplicationProcessState::class.java)) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt b/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt index df215590fc8..a05e64a32df 100644 --- a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt +++ b/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt @@ -15,9 +15,9 @@ package com.google.firebase.perf.metrics fun getTraceCounter(trace: Trace): Map { - return trace.getCounters() + return trace.getCounters() } fun getTraceCounterCount(trace: Trace, counterName: String): Long { - return trace.getCounters().get(counterName)!!.getCount() + return trace.getCounters().get(counterName)!!.getCount() } diff --git a/firebase-perf/src/main/AndroidManifest.xml b/firebase-perf/src/main/AndroidManifest.xml index 48ae28f8c6e..c40a7de8422 100644 --- a/firebase-perf/src/main/AndroidManifest.xml +++ b/firebase-perf/src/main/AndroidManifest.xml @@ -6,11 +6,6 @@ - Responsible for initializing the AppStartTrace, and early initialization of ConfigResolver + * + * @hide + */ +public class FirebasePerfEarly { + + public FirebasePerfEarly( + FirebaseApp app, @Nullable StartupTime startupTime, Executor uiExecutor) { + Context context = app.getApplicationContext(); + + // Initialize ConfigResolver early for accessing device caching layer. + ConfigResolver configResolver = ConfigResolver.getInstance(); + configResolver.setApplicationContext(context); + + AppStateMonitor appStateMonitor = AppStateMonitor.getInstance(); + appStateMonitor.registerActivityLifecycleCallbacks(context); + appStateMonitor.registerForAppColdStart(new FirebasePerformanceInitializer()); + + if (startupTime != null) { + AppStartTrace appStartTrace = AppStartTrace.getInstance(); + appStartTrace.registerActivityLifecycleCallbacks(context); + uiExecutor.execute(new AppStartTrace.StartFromBackgroundRunnable(appStartTrace)); + } + + // In the case of cold start, we create a session and start collecting gauges as early as + // possible. + // There is code in SessionManager that prevents us from resetting the session twice in case + // of app cold start. + SessionManager.getInstance().initializeGaugeCollection(); + } +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java index 3dc56578866..c01f035af1f 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java @@ -17,10 +17,13 @@ import androidx.annotation.Keep; import com.google.android.datatransport.TransportFactory; import com.google.firebase.FirebaseApp; +import com.google.firebase.StartupTime; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentContainer; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.perf.injection.components.DaggerFirebasePerformanceComponent; import com.google.firebase.perf.injection.components.FirebasePerformanceComponent; @@ -29,6 +32,7 @@ import com.google.firebase.remoteconfig.RemoteConfigComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** * {@link com.google.firebase.components.ComponentRegistrar} for the Firebase Performance SDK. @@ -41,10 +45,12 @@ @Keep public class FirebasePerfRegistrar implements ComponentRegistrar { private static final String LIBRARY_NAME = "fire-perf"; + private static final String EARLY_LIBRARY_NAME = "fire-perf-early"; @Override @Keep public List> getComponents() { + Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); return Arrays.asList( Component.builder(FirebasePerformance.class) .name(LIBRARY_NAME) @@ -52,8 +58,22 @@ public List> getComponents() { .add(Dependency.requiredProvider(RemoteConfigComponent.class)) .add(Dependency.required(FirebaseInstallationsApi.class)) .add(Dependency.requiredProvider(TransportFactory.class)) + .add(Dependency.required(FirebasePerfEarly.class)) .factory(FirebasePerfRegistrar::providesFirebasePerformance) .build(), + Component.builder(FirebasePerfEarly.class) + .name(EARLY_LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.optionalProvider(StartupTime.class)) + .add(Dependency.required(uiExecutor)) + .eagerInDefaultApp() + .factory( + container -> + new FirebasePerfEarly( + container.get(FirebaseApp.class), + container.getProvider(StartupTime.class).get(), + container.get(uiExecutor))) + .build(), /** * Fireperf SDK is lazily by {@link FirebasePerformanceInitializer} during {@link * com.google.firebase.perf.application.AppStateMonitor#onActivityResumed(Activity)}. we use @@ -65,6 +85,8 @@ public List> getComponents() { } private static FirebasePerformance providesFirebasePerformance(ComponentContainer container) { + // Ensure FirebasePerfEarly was initialized + container.get(FirebasePerfEarly.class); FirebasePerformanceComponent component = DaggerFirebasePerformanceComponent.builder() .firebasePerformanceModule( diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/application/FrameMetricsRecorder.java b/firebase-perf/src/main/java/com/google/firebase/perf/application/FrameMetricsRecorder.java index dc7052b4049..9d63dce21cb 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/application/FrameMetricsRecorder.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/application/FrameMetricsRecorder.java @@ -15,6 +15,7 @@ package com.google.firebase.perf.application; import android.app.Activity; +import android.os.Build; import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; import androidx.fragment.app.Fragment; @@ -107,11 +108,22 @@ public Optional stop() { } Optional data = this.snapshot(); try { + // No reliable way to check for hardware-acceleration, so we must catch retroactively (#2736). frameMetricsAggregator.remove(activity); - } catch (IllegalArgumentException err) { + } catch (IllegalArgumentException | NullPointerException ex) { + // Both of these exceptions result from android.view.View.addFrameMetricsListener silently + // failing when the view is not hardware-accelerated. Successful addFrameMetricsListener + // stores an observer in a list, and initializes the list if it was uninitialized. Invoking + // View.removeFrameMetricsListener(listener) throws IAE if it doesn't exist in the list, or + // throws NPE if the list itself was never initialized (#4184). + if (ex instanceof NullPointerException && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + // Re-throw above API 28, since the NPE is fixed in API 29: + // https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b + throw ex; + } logger.warn( - "View not hardware accelerated. Unable to collect FrameMetrics. %s", err.toString()); - return Optional.absent(); + "View not hardware accelerated. Unable to collect FrameMetrics. %s", ex.toString()); + data = Optional.absent(); } frameMetricsAggregator.reset(); isRecording = false; diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/config/DeviceCacheManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/config/DeviceCacheManager.java index 012edfc8b81..e62f7e248f4 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/config/DeviceCacheManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/config/DeviceCacheManager.java @@ -14,6 +14,7 @@ package com.google.firebase.perf.config; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.Nullable; @@ -49,6 +50,8 @@ public DeviceCacheManager(ExecutorService serialExecutor) { this.serialExecutor = serialExecutor; } + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public static synchronized DeviceCacheManager getInstance() { if (instance == null) { instance = new DeviceCacheManager(Executors.newSingleThreadExecutor()); diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/config/RemoteConfigManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/config/RemoteConfigManager.java index 7c27b59c3d9..d4166dd1c70 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/config/RemoteConfigManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/config/RemoteConfigManager.java @@ -16,15 +16,17 @@ import static com.google.firebase.perf.config.ConfigurationConstants.ExperimentTTID; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import androidx.annotation.Keep; import androidx.annotation.Nullable; import com.google.android.gms.common.util.VisibleForTesting; +import com.google.firebase.FirebaseApp; +import com.google.firebase.StartupTime; import com.google.firebase.inject.Provider; import com.google.firebase.perf.logging.AndroidLogger; -import com.google.firebase.perf.provider.FirebasePerfProvider; import com.google.firebase.perf.util.Optional; import com.google.firebase.remoteconfig.FirebaseRemoteConfig; import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue; @@ -66,6 +68,8 @@ public class RemoteConfigManager { @Nullable private Provider firebaseRemoteConfigProvider; @Nullable private FirebaseRemoteConfig firebaseRemoteConfig; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private RemoteConfigManager() { this( DeviceCacheManager.getInstance(), @@ -77,7 +81,19 @@ private RemoteConfigManager() { new LinkedBlockingQueue()), /* firebaseRemoteConfig= */ null, // set once FirebaseRemoteConfig is initialized MIN_APP_START_CONFIG_FETCH_DELAY_MS - + new Random().nextInt(RANDOM_APP_START_CONFIG_FETCH_DELAY_MS)); + + new Random().nextInt(RANDOM_APP_START_CONFIG_FETCH_DELAY_MS), + getInitialStartupMillis()); + } + + @VisibleForTesting + @SuppressWarnings("FirebaseUseExplicitDependencies") + static long getInitialStartupMillis() { + StartupTime startupTime = FirebaseApp.getInstance().get(StartupTime.class); + if (startupTime != null) { + return startupTime.getEpochMillis(); + } else { + return System.currentTimeMillis(); + } } @VisibleForTesting @@ -85,7 +101,8 @@ private RemoteConfigManager() { DeviceCacheManager cache, Executor executor, FirebaseRemoteConfig firebaseRemoteConfig, - long appStartConfigFetchDelayInMs) { + long appStartConfigFetchDelayInMs, + long appStartTimeInMs) { this.cache = cache; this.executor = executor; this.firebaseRemoteConfig = firebaseRemoteConfig; @@ -93,8 +110,7 @@ private RemoteConfigManager() { firebaseRemoteConfig == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(firebaseRemoteConfig.getAll()); - this.appStartTimeInMs = - TimeUnit.MICROSECONDS.toMillis(FirebasePerfProvider.getAppStartTime().getMicros()); + this.appStartTimeInMs = appStartTimeInMs; this.appStartConfigFetchDelayInMs = appStartConfigFetchDelayInMs; } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java index 2e1bb5cd8d3..2d91f2e2262 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java @@ -14,6 +14,7 @@ package com.google.firebase.perf.metrics; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.app.Application.ActivityLifecycleCallbacks; @@ -26,15 +27,17 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.common.util.VisibleForTesting; +import com.google.firebase.FirebaseApp; +import com.google.firebase.StartupTime; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.logging.AndroidLogger; -import com.google.firebase.perf.provider.FirebasePerfProvider; import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.FirstDrawDoneListener; +import com.google.firebase.perf.util.PreDrawListener; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.ApplicationProcessState; import com.google.firebase.perf.v1.TraceMetric; @@ -75,6 +78,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks { private final TransportManager transportManager; private final Clock clock; private final ConfigResolver configResolver; + private final TraceMetric.Builder experimentTtid; private Context appContext; /** * The first time onCreate() of any activity is called, the activity is saved as launchActivity. @@ -91,11 +95,14 @@ public class AppStartTrace implements ActivityLifecycleCallbacks { */ private boolean isTooLateToInitUI = false; + private static Timer firebaseStartupTime = null; + private Timer appStartTime = null; private Timer onCreateTime = null; private Timer onStartTime = null; private Timer onResumeTime = null; private Timer firstDrawDone = null; + private Timer preDraw = null; private PerfSession startSession; private boolean isStartedFromBackground = false; @@ -133,6 +140,8 @@ public static AppStartTrace getInstance() { return instance != null ? instance : getInstance(TransportManager.getInstance(), new Clock()); } + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") static AppStartTrace getInstance(TransportManager transportManager, Clock clock) { if (instance == null) { synchronized (AppStartTrace.class) { @@ -154,6 +163,7 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock) return instance; } + @SuppressWarnings("FirebaseUseExplicitDependencies") AppStartTrace( @NonNull TransportManager transportManager, @NonNull Clock clock, @@ -163,9 +173,18 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock) this.clock = clock; this.configResolver = configResolver; this.executorService = executorService; + + StartupTime startupTime = FirebaseApp.getInstance().get(StartupTime.class); + if (startupTime == null) { + firebaseStartupTime = new Timer(); + } else { + firebaseStartupTime = + Timer.ofElapsedRealtime(startupTime.getElapsedRealtime(), startupTime.getUptimeMillis()); + } + this.experimentTtid = TraceMetric.newBuilder().setName("_experiment_app_start_ttid"); } - /** Called from FirebasePerfProvider to register this callback. */ + /** Called from FirebasePerfEarly to register this callback. */ public synchronized void registerActivityLifecycleCallbacks(@NonNull Context context) { // Make sure the callback is registered only once. if (isRegisteredForLifecycleCallbacks) { @@ -190,31 +209,88 @@ public synchronized void unregisterActivityLifecycleCallbacks() { /** * Gets the timetamp that marks the beginning of app start, currently defined as the beginning of - * BIND_APPLICATION. Fallback to class-load time of {@link FirebasePerfProvider} when API < 24. + * BIND_APPLICATION. Fallback to class-load time of {@link StartupTime} when API < 24. * * @return {@link Timer} at the beginning of app start by Fireperf definition. */ private static Timer getStartTimer() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Timer.ofElapsedRealtime(Process.getStartElapsedRealtime()); + return Timer.ofElapsedRealtime( + Process.getStartElapsedRealtime(), Process.getStartUptimeMillis()); } - return FirebasePerfProvider.getAppStartTime(); + return firebaseStartupTime; } private void recordFirstDrawDone() { if (firstDrawDone != null) { return; } + Timer start = getStartTimer(); this.firstDrawDone = clock.getTime(); - executorService.execute( - () -> this.logColdStart(getStartTimer(), this.firstDrawDone, this.startSession)); + this.experimentTtid + .setClientStartTimeUs(start.getMicros()) + .setDurationUs(start.getDurationMicros(this.firstDrawDone)); - if (isRegisteredForLifecycleCallbacks) { - // After AppStart trace is queued to be logged, we can unregister this callback. - unregisterActivityLifecycleCallbacks(); + TraceMetric.Builder subtrace = + TraceMetric.newBuilder() + .setName("_experiment_classLoadTime") + .setClientStartTimeUs(firebaseStartupTime.getMicros()) + .setDurationUs(firebaseStartupTime.getDurationMicros(this.firstDrawDone)); + this.experimentTtid.addSubtraces(subtrace.build()); + + subtrace = TraceMetric.newBuilder(); + subtrace + .setName("_experiment_uptimeMillis") + .setClientStartTimeUs(start.getMicros()) + .setDurationUs(start.getDurationUptimeMicros(this.firstDrawDone)); + this.experimentTtid.addSubtraces(subtrace.build()); + + this.experimentTtid.addPerfSessions(this.startSession.build()); + + if (isExperimentTraceDone()) { + executorService.execute(() -> this.logExperimentTtid(this.experimentTtid)); + + if (isRegisteredForLifecycleCallbacks) { + // After AppStart trace is queued to be logged, we can unregister this callback. + unregisterActivityLifecycleCallbacks(); + } + } + } + + private void recordFirstDrawDonePreDraw() { + if (preDraw != null) { + return; + } + Timer start = getStartTimer(); + this.preDraw = clock.getTime(); + TraceMetric.Builder subtrace = + TraceMetric.newBuilder() + .setName("_experiment_preDraw") + .setClientStartTimeUs(start.getMicros()) + .setDurationUs(start.getDurationMicros(this.preDraw)); + this.experimentTtid.addSubtraces(subtrace.build()); + + subtrace = TraceMetric.newBuilder(); + subtrace + .setName("_experiment_preDraw_uptimeMillis") + .setClientStartTimeUs(start.getMicros()) + .setDurationUs(start.getDurationUptimeMicros(this.preDraw)); + this.experimentTtid.addSubtraces(subtrace.build()); + + if (isExperimentTraceDone()) { + executorService.execute(() -> this.logExperimentTtid(this.experimentTtid)); + + if (isRegisteredForLifecycleCallbacks) { + // After AppStart trace is queued to be logged, we can unregister this callback. + unregisterActivityLifecycleCallbacks(); + } } } + private boolean isExperimentTraceDone() { + return this.preDraw != null && this.firstDrawDone != null; + } + @Override public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate() @@ -225,8 +301,7 @@ public synchronized void onActivityCreated(Activity activity, Bundle savedInstan launchActivity = new WeakReference(activity); onCreateTime = clock.getTime(); - if (FirebasePerfProvider.getAppStartTime().getDurationMicros(onCreateTime) - > MAX_LATENCY_BEFORE_UI_INIT) { + if (firebaseStartupTime.getDurationMicros(onCreateTime) > MAX_LATENCY_BEFORE_UI_INIT) { isTooLateToInitUI = true; } } @@ -252,6 +327,7 @@ public synchronized void onActivityResumed(Activity activity) { if (isExperimentTTIDEnabled) { View rootView = activity.findViewById(android.R.id.content); FirstDrawDoneListener.registerForNextDraw(rootView, this::recordFirstDrawDone); + PreDrawListener.registerForNextDraw(rootView, this::recordFirstDrawDonePreDraw); } if (onResumeTime != null) { // An activity already called onResume() @@ -261,7 +337,7 @@ public synchronized void onActivityResumed(Activity activity) { appStartActivity = new WeakReference(activity); onResumeTime = clock.getTime(); - this.appStartTime = FirebasePerfProvider.getAppStartTime(); + this.appStartTime = firebaseStartupTime; this.startSession = SessionManager.getInstance().perfSession(); AndroidLogger.getInstance() .debug( @@ -280,21 +356,7 @@ public synchronized void onActivityResumed(Activity activity) { } } - private void logColdStart(Timer start, Timer end, PerfSession session) { - TraceMetric.Builder metric = - TraceMetric.newBuilder() - .setName("_experiment_app_start_ttid") - .setClientStartTimeUs(start.getMicros()) - .setDurationUs(start.getDurationMicros(end)); - - TraceMetric.Builder subtrace = - TraceMetric.newBuilder() - .setName("_experiment_classLoadTime") - .setClientStartTimeUs(FirebasePerfProvider.getAppStartTime().getMicros()) - .setDurationUs(FirebasePerfProvider.getAppStartTime().getDurationMicros(end)); - - metric.addSubtraces(subtrace).addPerfSessions(this.startSession.build()); - + private void logExperimentTtid(TraceMetric.Builder metric) { transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND); } @@ -333,10 +395,32 @@ private void logAppStartTrace() { } @Override - public void onActivityPaused(Activity activity) {} + public void onActivityPaused(Activity activity) { + if (isExperimentTraceDone()) { + return; + } + Timer onPauseTime = clock.getTime(); + TraceMetric.Builder subtrace = + TraceMetric.newBuilder() + .setName("_experiment_onPause") + .setClientStartTimeUs(onPauseTime.getMicros()) + .setDurationUs(getStartTimer().getDurationMicros(onPauseTime)); + this.experimentTtid.addSubtraces(subtrace.build()); + } @Override - public synchronized void onActivityStopped(Activity activity) {} + public void onActivityStopped(Activity activity) { + if (isExperimentTraceDone()) { + return; + } + Timer onStopTime = clock.getTime(); + TraceMetric.Builder subtrace = + TraceMetric.newBuilder() + .setName("_experiment_onStop") + .setClientStartTimeUs(onStopTime.getMicros()) + .setDurationUs(getStartTimer().getDurationMicros(onStopTime)); + this.experimentTtid.addSubtraces(subtrace.build()); + } @Override public void onActivityDestroyed(Activity activity) {} @@ -347,8 +431,8 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} /** * We use StartFromBackgroundRunnable to detect if app is started from background or foreground. * If app is started from background, we do not generate AppStart trace. This runnable is posted - * to main UI thread from FirebasePerfProvider. If app is started from background, this runnable - * will be executed before any activity's onCreate() method. If app is started from foreground, + * to main UI thread from FirebasePerfEarly. If app is started from background, this runnable will + * be executed before any activity's onCreate() method. If app is started from foreground, * activity's onCreate() method is executed before this runnable. */ public static class StartFromBackgroundRunnable implements Runnable { diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java index 50f35f3ae4a..2bde865a0c8 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java @@ -689,8 +689,7 @@ public Map getAttributes() { * Describes the kinds of special objects contained in this Parcelable's marshalled * representation. * - * @see - * https://developer.android.com/reference/android/os/Parcelable.html + * @see Parcelable * @return always returns 0. */ @Keep diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/provider/FirebasePerfProvider.java b/firebase-perf/src/main/java/com/google/firebase/perf/provider/FirebasePerfProvider.java deleted file mode 100644 index cd3df8158fb..00000000000 --- a/firebase-perf/src/main/java/com/google/firebase/perf/provider/FirebasePerfProvider.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2020 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.perf.provider; - -import android.app.Application; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.common.util.VisibleForTesting; -import com.google.firebase.perf.FirebasePerformanceInitializer; -import com.google.firebase.perf.application.AppStateMonitor; -import com.google.firebase.perf.config.ConfigResolver; -import com.google.firebase.perf.metrics.AppStartTrace; -import com.google.firebase.perf.session.SessionManager; -import com.google.firebase.perf.util.Clock; -import com.google.firebase.perf.util.Timer; - -/** Initializes app start time at app startup time. */ -@Keep -public class FirebasePerfProvider extends ContentProvider { - - private static final Timer APP_START_TIME = new Clock().getTime(); - /** Should match the {@link FirebasePerfProvider} authority if $androidId is empty. */ - @VisibleForTesting - static final String EMPTY_APPLICATION_ID_PROVIDER_AUTHORITY = - "com.google.firebase.firebaseperfprovider"; - - private final Handler mainHandler = new Handler(Looper.getMainLooper()); - - public static Timer getAppStartTime() { - return APP_START_TIME; - } - - @Override - public void attachInfo(Context context, ProviderInfo info) { - // super.attachInfo calls onCreate(). Fail as early as possible. - checkContentProviderAuthority(info); - super.attachInfo(context, info); - - // Initialize ConfigResolver early for accessing device caching layer. - ConfigResolver configResolver = ConfigResolver.getInstance(); - configResolver.setContentProviderContext(getContext()); - - AppStateMonitor appStateMonitor = AppStateMonitor.getInstance(); - appStateMonitor.registerActivityLifecycleCallbacks(getContext()); - appStateMonitor.registerForAppColdStart(new FirebasePerformanceInitializer()); - - AppStartTrace appStartTrace = AppStartTrace.getInstance(); - appStartTrace.registerActivityLifecycleCallbacks(getContext()); - - mainHandler.post(new AppStartTrace.StartFromBackgroundRunnable(appStartTrace)); - - // In the case of cold start, we create a session and start collecting gauges as early as - // possible. - // There is code in SessionManager that prevents us from resetting the session twice in case - // of app cold start. - SessionManager.getInstance().initializeGaugeCollection(); - } - - /** Called before {@link Application#onCreate()}. */ - @Override - public boolean onCreate() { - return false; - } - - /** - * Check that the content provider's authority does not use firebase-common's package name. If it - * does, crash in order to alert the developer of the problem before they distribute the app. - */ - private static void checkContentProviderAuthority(@NonNull ProviderInfo info) { - Preconditions.checkNotNull(info, "FirebasePerfProvider ProviderInfo cannot be null."); - if (EMPTY_APPLICATION_ID_PROVIDER_AUTHORITY.equals(info.authority)) { - throw new IllegalStateException( - "Incorrect provider authority in manifest. Most likely due to a missing " - + "applicationId variable in application's build.gradle."); - } - } - - @Nullable - @Override - public Cursor query( - Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - return null; - } - - @Nullable - @Override - public String getType(Uri uri) { - return null; - } - - @Nullable - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - return 0; - } -} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java index b482268adb3..ad9b1c04d00 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java @@ -79,6 +79,8 @@ public void setApplicationContext(final Context appContext) { // Get PerfSession in main thread first, because it is possible that app changes fg/bg state // which creates a new perfSession, before the following is executed in background thread final PerfSession appStartSession = perfSession; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") ExecutorService executorService = Executors.newSingleThreadExecutor(); syncInitFuture = executorService.submit( diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java index c34fd31dc72..e33d363c0aa 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java @@ -16,6 +16,7 @@ import static android.system.Os.sysconf; +import android.annotation.SuppressLint; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.system.OsConstants; @@ -80,6 +81,8 @@ public class CpuGaugeCollector { @Nullable private ScheduledFuture cpuMetricCollectorJob = null; private long cpuMetricCollectionRateMs = UNSET_CPU_METRIC_COLLECTION_RATE; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") CpuGaugeCollector() { cpuMetricReadings = new ConcurrentLinkedQueue<>(); cpuMetricCollectorExecutor = Executors.newSingleThreadScheduledExecutor(); diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java index 64cd5998a3a..0d15d3519c1 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java @@ -14,6 +14,7 @@ package com.google.firebase.perf.session.gauges; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.Keep; import androidx.annotation.Nullable; @@ -63,9 +64,11 @@ public class GaugeManager { private ApplicationProcessState applicationProcessState = ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private GaugeManager() { this( - new Lazy<>(() -> Executors.newSingleThreadScheduledExecutor()), + new Lazy<>(Executors::newSingleThreadScheduledExecutor), TransportManager.getInstance(), ConfigResolver.getInstance(), null, diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java index 8c3a3706a19..b221ba07760 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java @@ -14,6 +14,7 @@ package com.google.firebase.perf.session.gauges; +import android.annotation.SuppressLint; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.logging.AndroidLogger; @@ -52,6 +53,8 @@ public class MemoryGaugeCollector { @Nullable private ScheduledFuture memoryMetricCollectorJob = null; private long memoryMetricCollectionRateMs = UNSET_MEMORY_METRIC_COLLECTION_RATE; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") MemoryGaugeCollector() { this(Executors.newSingleThreadScheduledExecutor(), Runtime.getRuntime()); } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java index fab187ee6bf..dc99c77aa42 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java @@ -17,6 +17,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; +import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -125,6 +126,8 @@ public class TransportManager implements AppStateCallback { private boolean isForegroundState = false; + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private TransportManager() { // MAX_POOL_SIZE must always be 1. We only allow one thread in this Executor. The reason // we specifically use a ThreadPoolExecutor rather than generating one from ExecutorService diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java b/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java index 740278ad4a4..f6c7d0a8b84 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java @@ -13,24 +13,23 @@ // limitations under the License. package com.google.firebase.perf.util; +import android.annotation.SuppressLint; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.ViewTreeObserver; -import androidx.annotation.RequiresApi; import java.util.concurrent.atomic.AtomicReference; /** * OnDrawListener that unregisters itself and invokes callback when the next draw is done. This API - * 16+ implementation is an approximation of the initial display time. {@link - * android.view.Choreographer#postFrameCallback} is an Android API that provides a simpler and more - * accurate initial display time, but it was bugged before API 30, hence we use this backported - * implementation. + * 16+ implementation is an approximation of the initial-display-time defined by Android Vitals. */ -@RequiresApi(Build.VERSION_CODES.JELLY_BEAN) public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener { + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + private final AtomicReference viewReference; private final Runnable callback; diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/util/PreDrawListener.java b/firebase-perf/src/main/java/com/google/firebase/perf/util/PreDrawListener.java new file mode 100644 index 00000000000..2035d6f79e5 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/util/PreDrawListener.java @@ -0,0 +1,57 @@ +// Copyright 2022 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.perf.util; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.ViewTreeObserver; +import java.util.concurrent.atomic.AtomicReference; + +/** + * OnPreDraw listener that unregisters itself and post a callback to the main thread during + * OnPreDraw. This is an approximation of the initial-display time defined by Android Vitals. + */ +public class PreDrawListener implements ViewTreeObserver.OnPreDrawListener { + // TODO(b/258263016): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + private final AtomicReference viewReference; + private final Runnable callback; + + /** Registers a post-draw callback for the next draw of a view. */ + public static void registerForNextDraw(View view, Runnable drawDoneCallback) { + final PreDrawListener listener = new PreDrawListener(view, drawDoneCallback); + view.getViewTreeObserver().addOnPreDrawListener(listener); + } + + private PreDrawListener(View view, Runnable callback) { + this.viewReference = new AtomicReference<>(view); + this.callback = callback; + } + + @Override + public boolean onPreDraw() { + // Set viewReference to null so any onPreDraw past the first is a no-op + View view = viewReference.getAndSet(null); + if (view == null) { + return true; + } + view.getViewTreeObserver().removeOnPreDrawListener(this); + mainThreadHandler.post(callback); + return true; + } +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/util/Timer.java b/firebase-perf/src/main/java/com/google/firebase/perf/util/Timer.java index 5ab5539e832..56f21955851 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/util/Timer.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/util/Timer.java @@ -43,6 +43,11 @@ public class Timer implements Parcelable { * compute duration between 2 timestamps in the same timebase. It is NOT wall-clock time. */ private long elapsedRealtimeMicros; + /** + * Monotonic time measured in the {@link SystemClock#uptimeMillis()} timebase. Only used to + * compute duration between 2 timestamps in the same timebase. It is NOT wall-clock time. + */ + private long uptimeMicros; /** * Returns a new Timer object as if it was stamped at the given elapsedRealtime. Uses current @@ -50,10 +55,11 @@ public class Timer implements Parcelable { * * @param elapsedRealtimeMillis timestamp in the {@link SystemClock#elapsedRealtime()} timebase */ - public static Timer ofElapsedRealtime(final long elapsedRealtimeMillis) { + public static Timer ofElapsedRealtime(final long elapsedRealtimeMillis, final long uptimeMillis) { + long uptimeMicros = MILLISECONDS.toMicros(uptimeMillis); long elapsedRealtimeMicros = MILLISECONDS.toMicros(elapsedRealtimeMillis); long wallClockMicros = wallClockMicros() + (elapsedRealtimeMicros - elapsedRealtimeMicros()); - return new Timer(wallClockMicros, elapsedRealtimeMicros); + return new Timer(wallClockMicros, elapsedRealtimeMicros, uptimeMicros); } /** @@ -77,10 +83,14 @@ private static long elapsedRealtimeMicros() { return MILLISECONDS.toMicros(SystemClock.elapsedRealtime()); } + private static long uptimeMicros() { + return MILLISECONDS.toMicros(SystemClock.uptimeMillis()); + } + // TODO: make all constructors private, use public static factory methods, per Effective Java /** Construct Timer object using System clock. */ public Timer() { - this(wallClockMicros(), elapsedRealtimeMicros()); + this(wallClockMicros(), elapsedRealtimeMicros(), uptimeMicros()); } /** @@ -91,9 +101,10 @@ public Timer() { * SystemClock#elapsedRealtime()} timebase */ @VisibleForTesting - Timer(long epochMicros, long elapsedRealtimeMicros) { + Timer(long epochMicros, long elapsedRealtimeMicros, long uptimeMicros) { this.wallClockMicros = epochMicros; this.elapsedRealtimeMicros = elapsedRealtimeMicros; + this.uptimeMicros = uptimeMicros; } /** @@ -104,11 +115,11 @@ public Timer() { */ @VisibleForTesting public Timer(long testTime) { - this(testTime, testTime); + this(testTime, testTime, testTime); } private Timer(Parcel in) { - this(in.readLong(), in.readLong()); + this(in.readLong(), in.readLong(), in.readLong()); } /** resets the start time */ @@ -116,6 +127,7 @@ public void reset() { // TODO: consider removing this method and make Timer immutable thus fully thread-safe wallClockMicros = wallClockMicros(); elapsedRealtimeMicros = elapsedRealtimeMicros(); + uptimeMicros = uptimeMicros(); } /** Return wall-clock time in microseconds. */ @@ -144,6 +156,16 @@ public long getDurationMicros(@NonNull final Timer end) { return end.elapsedRealtimeMicros - this.elapsedRealtimeMicros; } + /** + * Calculate duration in microseconds using uptime. The start time is this Timer object. + * + * @param end end Timer object + * @return duration in microseconds. + */ + public long getDurationUptimeMicros(@NonNull final Timer end) { + return end.uptimeMicros - this.uptimeMicros; + } + /** * Calculates the current wall clock off the existing wall clock time. The reason this is better * instead of just doing System.getCurrentTimeMillis is that the device time could've changed @@ -166,6 +188,7 @@ public long getCurrentTimestampMicros() { public void writeToParcel(Parcel out, int flags) { out.writeLong(wallClockMicros); out.writeLong(elapsedRealtimeMicros); + out.writeLong(uptimeMicros); } /** diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java index 8fd5c68d354..7df39fe6a1e 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java @@ -18,11 +18,15 @@ import com.google.android.datatransport.TransportFactory; import com.google.firebase.FirebaseApp; +import com.google.firebase.StartupTime; +import com.google.firebase.annotations.concurrent.UiThread; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.remoteconfig.RemoteConfigComponent; import java.util.List; +import java.util.concurrent.Executor; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -35,9 +39,7 @@ public void testGetComponents() { FirebasePerfRegistrar firebasePerfRegistrar = new FirebasePerfRegistrar(); List> components = firebasePerfRegistrar.getComponents(); - // Note: Although we have 3 deps but looks like size doesn't count deps towards interface like - // FirebaseInstallationsApi - assertThat(components).hasSize(2); + assertThat(components).hasSize(3); Component firebasePerfComponent = components.get(0); @@ -46,8 +48,19 @@ public void testGetComponents() { Dependency.required(FirebaseApp.class), Dependency.requiredProvider(RemoteConfigComponent.class), Dependency.required(FirebaseInstallationsApi.class), - Dependency.requiredProvider(TransportFactory.class)); + Dependency.requiredProvider(TransportFactory.class), + Dependency.required(FirebasePerfEarly.class)); assertThat(firebasePerfComponent.isLazy()).isTrue(); + + Component firebasePerfEarlyComponent = components.get(1); + + assertThat(firebasePerfEarlyComponent.getDependencies()) + .containsExactly( + Dependency.required(Qualified.qualified(UiThread.class, Executor.class)), + Dependency.required(FirebaseApp.class), + Dependency.optionalProvider(StartupTime.class)); + + assertThat(firebasePerfEarlyComponent.isLazy()).isFalse(); } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java index 500f0ad146b..138363bd29a 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java @@ -15,6 +15,7 @@ package com.google.firebase.perf.application; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.perf.application.AppStateMonitor.AppStateCallback; import static com.google.firebase.perf.v1.ApplicationProcessState.FOREGROUND_BACKGROUND; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; @@ -670,9 +671,11 @@ public void activityStateChanges_singleSubscriber_callbackIsCalled() { AppStateMonitor monitor = new AppStateMonitor(transportManager, clock); Map subscriberState = new HashMap<>(); + // Register callbacks, but note that each callback is saved in a local variable. Otherwise + // WeakReference can get garbage collected, making this test flaky. final int subscriber1 = 1; - monitor.registerForAppState( - new WeakReference<>(newState -> subscriberState.put(subscriber1, newState))); + AppStateCallback callback1 = newState -> subscriberState.put(subscriber1, newState); + monitor.registerForAppState(new WeakReference<>(callback1)); // Activity comes to Foreground monitor.onActivityResumed(activity1); @@ -688,17 +691,19 @@ public void activityStateChanges_multipleSubscribers_callbackCalledOnEachSubscri AppStateMonitor monitor = new AppStateMonitor(transportManager, clock); Map subscriberState = new HashMap<>(); + // Register callbacks, but note that each callback is saved in a local variable. Otherwise + // WeakReference can get garbage collected, making this test flaky. final int subscriber1 = 1; - monitor.registerForAppState( - new WeakReference<>(newState -> subscriberState.put(subscriber1, newState))); + AppStateCallback callback1 = newState -> subscriberState.put(subscriber1, newState); + monitor.registerForAppState(new WeakReference<>(callback1)); final int subscriber2 = 2; - monitor.registerForAppState( - new WeakReference<>(newState -> subscriberState.put(subscriber2, newState))); + AppStateCallback callback2 = newState -> subscriberState.put(subscriber2, newState); + monitor.registerForAppState(new WeakReference<>(callback2)); final int subscriber3 = 3; - monitor.registerForAppState( - new WeakReference<>(newState -> subscriberState.put(subscriber3, newState))); + AppStateCallback callback3 = newState -> subscriberState.put(subscriber3, newState); + monitor.registerForAppState(new WeakReference<>(callback3)); // Activity comes to Foreground monitor.onActivityResumed(activity1); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/config/RemoteConfigManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/config/RemoteConfigManagerTest.java index af1b971d9a1..91db4227536 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/config/RemoteConfigManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/config/RemoteConfigManagerTest.java @@ -28,7 +28,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.inject.Provider; import com.google.firebase.perf.FirebasePerformanceTestBase; -import com.google.firebase.perf.provider.FirebasePerfProvider; import com.google.firebase.remoteconfig.FirebaseRemoteConfig; import com.google.firebase.remoteconfig.FirebaseRemoteConfigInfo; import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings; @@ -840,8 +839,7 @@ public void triggerRemoteConfigFetchIfNecessary_doesNotFetchBeforeAppStartRandom appStartConfigFetchDelay)); // Simulate time fast forward to some time before fetch time is up - long appStartTimeInMs = - TimeUnit.MICROSECONDS.toMillis(FirebasePerfProvider.getAppStartTime().getMicros()); + long appStartTimeInMs = System.currentTimeMillis(); when(remoteConfigManagerPartialMock.getCurrentSystemTimeMillis()) .thenReturn(appStartTimeInMs + appStartConfigFetchDelay - 2000); @@ -867,8 +865,7 @@ public void triggerRemoteConfigFetchIfNecessary_fetchesAfterAppStartRandomDelay( appStartConfigFetchDelay)); // Simulate time fast forward to 2s after fetch delay time is up - long appStartTimeInMs = - TimeUnit.MICROSECONDS.toMillis(FirebasePerfProvider.getAppStartTime().getMicros()); + long appStartTimeInMs = System.currentTimeMillis(); when(remoteConfigManagerPartialMock.getCurrentSystemTimeMillis()) .thenReturn(appStartTimeInMs + appStartConfigFetchDelay + 2000); @@ -920,13 +917,18 @@ private RemoteConfigManager setupTestRemoteConfigManager( when(mockFirebaseRemoteConfig.getAll()).thenReturn(configs); if (initializeFrc) { return new RemoteConfigManager( - cacheManager, fakeExecutor, mockFirebaseRemoteConfig, appStartConfigFetchDelayInMs); + cacheManager, + fakeExecutor, + mockFirebaseRemoteConfig, + appStartConfigFetchDelayInMs, + RemoteConfigManager.getInitialStartupMillis()); } else { return new RemoteConfigManager( cacheManager, fakeExecutor, /* firebaseRemoteConfig= */ null, - appStartConfigFetchDelayInMs); + appStartConfigFetchDelayInMs, + RemoteConfigManager.getInitialStartupMillis()); } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java index e705a145aed..65f4071ea81 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java @@ -15,7 +15,6 @@ package com.google.firebase.perf.metrics; import static com.google.common.truth.Truth.assertThat; -import static com.google.firebase.perf.util.TimerTest.getElapsedRealtimeMicros; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -26,7 +25,6 @@ import static org.robolectric.Shadows.shadowOf; import android.app.Activity; -import android.content.pm.ProviderInfo; import android.os.Bundle; import android.os.Looper; import android.os.Process; @@ -35,7 +33,6 @@ import androidx.test.core.app.ApplicationProvider; import com.google.firebase.perf.FirebasePerformanceTestBase; import com.google.firebase.perf.config.ConfigResolver; -import com.google.firebase.perf.provider.FirebasePerfProvider; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Clock; @@ -78,9 +75,6 @@ public class AppStartTraceTest extends FirebasePerformanceTestBase { // a mocked current wall-clock time in microseconds. private long currentTime = 0; - // Timer at the beginning of app startup - private Timer appStart; - @Before public void setUp() { initMocks(this); @@ -95,7 +89,6 @@ public Timer answer(InvocationOnMock invocationOnMock) throws Throwable { .getTime(); transportManager = mock(TransportManager.class); traceArgumentCaptor = ArgumentCaptor.forClass(TraceMetric.class); - appStart = FirebasePerfProvider.getAppStartTime(); } @After @@ -156,15 +149,10 @@ private void verifyFinalState( TraceMetric metric = traceArgumentCaptor.getValue(); Assert.assertEquals(Constants.TraceNames.APP_START_TRACE_NAME.toString(), metric.getName()); - Assert.assertEquals(appStart.getMicros(), metric.getClientStartTimeUs()); - Assert.assertEquals(resumeTime - getElapsedRealtimeMicros(appStart), metric.getDurationUs()); Assert.assertEquals(3, metric.getSubtracesCount()); Assert.assertEquals( Constants.TraceNames.ON_CREATE_TRACE_NAME.toString(), metric.getSubtraces(0).getName()); - Assert.assertEquals(appStart.getMicros(), metric.getSubtraces(0).getClientStartTimeUs()); - Assert.assertEquals( - createTime - getElapsedRealtimeMicros(appStart), metric.getSubtraces(0).getDurationUs()); Assert.assertEquals( Constants.TraceNames.ON_START_TRACE_NAME.toString(), metric.getSubtraces(1).getName()); @@ -225,7 +213,10 @@ public void testDelayedAppStart() { AppStartTrace trace = new AppStartTrace(transportManager, clock, configResolver, fakeExecutorService); // Delays activity creation after 1 minute from app start time. - currentTime = appStart.getMicros() + TimeUnit.MINUTES.toMicros(1) + 1; + currentTime = + TimeUnit.MILLISECONDS.toMicros(SystemClock.elapsedRealtime()) + + TimeUnit.MINUTES.toMicros(1) + + 1; trace.onActivityCreated(activity1, bundle); Assert.assertEquals(currentTime, trace.getOnCreateTime().getMicros()); ++currentTime; @@ -262,24 +253,6 @@ public void testStartFromBackground() { ArgumentMatchers.nullable(ApplicationProcessState.class)); } - @Test - public void testFirebasePerfProviderOnAttachInfo_initializesGaugeCollection() { - com.google.firebase.perf.session.PerfSession mockPerfSession = - mock(com.google.firebase.perf.session.PerfSession.class); - when(mockPerfSession.sessionId()).thenReturn("sessionId"); - when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true); - - SessionManager.getInstance().setPerfSession(mockPerfSession); - String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); - Assert.assertEquals(oldSessionId, SessionManager.getInstance().perfSession().sessionId()); - - FirebasePerfProvider provider = new FirebasePerfProvider(); - provider.attachInfo(ApplicationProvider.getApplicationContext(), new ProviderInfo()); - - Assert.assertEquals(oldSessionId, SessionManager.getInstance().perfSession().sessionId()); - verify(mockPerfSession, times(2)).isGaugeAndEventCollectionEnabled(); - } - @Test @Config(sdk = 26) public void timeToInitialDisplay_isLogged() { @@ -298,6 +271,11 @@ public void timeToInitialDisplay_isLogged() { trace.onActivityCreated(activity1, bundle); trace.onActivityStarted(activity1); trace.onActivityResumed(activity1); + // Experiment: simulate backgrounding before draw + trace.onActivityPaused(activity1); + trace.onActivityStopped(activity1); + trace.onActivityStarted(activity1); + trace.onActivityResumed(activity1); fakeExecutorService.runAll(); verify(transportManager, times(1)) .log(isA(TraceMetric.class), isA(ApplicationProcessState.class)); @@ -305,6 +283,7 @@ public void timeToInitialDisplay_isLogged() { // Simulate draw and manually stepping time forward ShadowSystemClock.advanceBy(Duration.ofMillis(1000)); long drawTime = TimeUnit.NANOSECONDS.toMicros(SystemClock.elapsedRealtimeNanos()); + testView.getViewTreeObserver().dispatchOnPreDraw(); testView.getViewTreeObserver().dispatchOnDraw(); shadowOf(Looper.getMainLooper()).idle(); fakeExecutorService.runNext(); @@ -317,5 +296,6 @@ public void timeToInitialDisplay_isLogged() { assertThat(ttid.getName()).isEqualTo("_experiment_app_start_ttid"); assertThat(ttid.getDurationUs()).isNotEqualTo(resumeTime - appStartTime); assertThat(ttid.getDurationUs()).isEqualTo(drawTime - appStartTime); + assertThat(ttid.getSubtracesCount()).isEqualTo(6); } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/util/TimerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/util/TimerTest.java index e8031b53577..c844bce04eb 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/util/TimerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/util/TimerTest.java @@ -53,8 +53,8 @@ public void ofElapsedRealtime_createsNewTimerWithArgumentElapsedRealtime() { // Robolectric shadows SystemClock, which is paused and can only change via specific methods. long refElapsedRealtime = SystemClock.elapsedRealtime(); Timer ref = new Timer(); - Timer past = Timer.ofElapsedRealtime(refElapsedRealtime - 100); - Timer future = Timer.ofElapsedRealtime(refElapsedRealtime + 100); + Timer past = Timer.ofElapsedRealtime(refElapsedRealtime - 100, 0); + Timer future = Timer.ofElapsedRealtime(refElapsedRealtime + 100, 0); assertThat(past.getDurationMicros(ref)).isEqualTo(MILLISECONDS.toMicros(100)); assertThat(ref.getDurationMicros(future)).isEqualTo(MILLISECONDS.toMicros(100)); @@ -67,10 +67,10 @@ public void ofElapsedRealtime_extrapolatesWallTime() { ShadowSystemClock.advanceBy(Duration.ofMillis(10000000)); long nowElapsedRealtime = SystemClock.elapsedRealtime(); Timer now = new Timer(); - Timer morePast = Timer.ofElapsedRealtime(nowElapsedRealtime - 2000); - Timer past = Timer.ofElapsedRealtime(nowElapsedRealtime - 1000); - Timer future = Timer.ofElapsedRealtime(nowElapsedRealtime + 1000); - Timer moreFuture = Timer.ofElapsedRealtime(nowElapsedRealtime + 2000); + Timer morePast = Timer.ofElapsedRealtime(nowElapsedRealtime - 2000, 0); + Timer past = Timer.ofElapsedRealtime(nowElapsedRealtime - 1000, 0); + Timer future = Timer.ofElapsedRealtime(nowElapsedRealtime + 1000, 0); + Timer moreFuture = Timer.ofElapsedRealtime(nowElapsedRealtime + 2000, 0); // We cannot manipulate System.currentTimeMillis() so multiple comparisons are used to test assertThat(morePast.getMicros()).isLessThan(past.getMicros()); @@ -103,7 +103,7 @@ public void testReset() throws InterruptedException { @Test public void testGetCurrentTimestampMicros() { - Timer timer = new Timer(0, 0); + Timer timer = new Timer(0, 0, 0); long currentTimeSmallest = timer.getCurrentTimestampMicros(); assertThat(timer.getMicros()).isEqualTo(0); @@ -112,7 +112,7 @@ public void testGetCurrentTimestampMicros() { @Test public void testParcel() { - Timer timer1 = new Timer(1000, 1000000); + Timer timer1 = new Timer(1000, 1000000, 1000000); Parcel p1 = Parcel.obtain(); timer1.writeToParcel(p1, 0); @@ -132,6 +132,6 @@ public void testParcel() { /** Helper for other tests that returns elapsedRealtimeMicros from a Timer object */ public static long getElapsedRealtimeMicros(Timer timer) { - return new Timer(0, 0).getDurationMicros(timer); + return new Timer(0, 0, 0).getDurationMicros(timer); } } diff --git a/firebase-segmentation/firebase-segmentation.gradle b/firebase-segmentation/firebase-segmentation.gradle index 215e75fa43e..c5081915348 100644 --- a/firebase-segmentation/firebase-segmentation.gradle +++ b/firebase-segmentation/firebase-segmentation.gradle @@ -38,6 +38,7 @@ android { } dependencies { + implementation project(':firebase-annotations') implementation project(':firebase-common') implementation project(':firebase-components') implementation project(':firebase-installations-interop') 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 b7e70bfef69..c9ba9550b38 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 @@ -30,7 +30,6 @@ 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 { @@ -43,24 +42,29 @@ public class FirebaseSegmentation { private final SegmentationServiceClient backendServiceClient; private final Executor executor; - FirebaseSegmentation(FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallationsApi) { + FirebaseSegmentation( + FirebaseApp firebaseApp, + FirebaseInstallationsApi firebaseInstallationsApi, + Executor blockingExecutor) { this( firebaseApp, firebaseInstallationsApi, new CustomInstallationIdCache(firebaseApp), - new SegmentationServiceClient(firebaseApp.getApplicationContext())); + new SegmentationServiceClient(firebaseApp.getApplicationContext()), + blockingExecutor); } FirebaseSegmentation( FirebaseApp firebaseApp, FirebaseInstallationsApi firebaseInstallationsApi, CustomInstallationIdCache localCache, - SegmentationServiceClient backendServiceClient) { + SegmentationServiceClient backendServiceClient, + Executor blockingExecutor) { this.firebaseApp = firebaseApp; this.firebaseInstallationsApi = firebaseInstallationsApi; this.localCache = localCache; this.backendServiceClient = backendServiceClient; - this.executor = Executors.newFixedThreadPool(4); + this.executor = blockingExecutor; } /** 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 d005b206677..1620d043ae3 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 @@ -16,13 +16,16 @@ import androidx.annotation.NonNull; import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.platforminfo.LibraryVersionComponent; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; /** @hide */ public class FirebaseSegmentationRegistrar implements ComponentRegistrar { @@ -31,15 +34,19 @@ public class FirebaseSegmentationRegistrar implements ComponentRegistrar { @Override @NonNull public List> getComponents() { + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); return Arrays.asList( Component.builder(FirebaseSegmentation.class) .name(LIBRARY_NAME) .add(Dependency.required(FirebaseApp.class)) .add(Dependency.required(FirebaseInstallationsApi.class)) + .add(Dependency.required(blockingExecutor)) .factory( c -> new FirebaseSegmentation( - c.get(FirebaseApp.class), c.get(FirebaseInstallationsApi.class))) + c.get(FirebaseApp.class), + c.get(FirebaseInstallationsApi.class), + c.get(blockingExecutor))) .build(), LibraryVersionComponent.create(LIBRARY_NAME, 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 6a8a37485c2..1e6fb7b7106 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 @@ -134,7 +134,11 @@ public void cleanUp() throws Exception { public void testUpdateCustomInstallationId_CacheOk_BackendOk() throws Exception { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsOk); + firebaseApp, + firebaseInstallationsApi, + actualCache, + backendClientReturnsOk, + taskExecutor); // No exception, means success. TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); @@ -153,7 +157,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_Retryable() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); + firebaseApp, + firebaseInstallationsApi, + actualCache, + backendClientReturnsError, + taskExecutor); // Expect exception try { @@ -185,7 +193,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() .thenReturn(SegmentationServiceClient.Code.CONFLICT); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); + firebaseApp, + firebaseInstallationsApi, + actualCache, + backendClientReturnsError, + taskExecutor); // Expect exception try { @@ -210,7 +222,11 @@ public void testUpdateCustomInstallationId_CacheOk_BackendError_NotRetryable() public void testUpdateCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, cacheReturnsError, backendClientReturnsOk); + firebaseApp, + firebaseInstallationsApi, + cacheReturnsError, + backendClientReturnsOk, + taskExecutor); // Expect exception try { @@ -237,7 +253,11 @@ public void testClearCustomInstallationId_CacheOk_BackendOk() throws Exception { CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsOk); + firebaseApp, + firebaseInstallationsApi, + actualCache, + backendClientReturnsOk, + taskExecutor); // No exception, means success. TestOnCompleteListener onCompleteListener = new TestOnCompleteListener<>(); @@ -258,7 +278,11 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio CustomInstallationIdCache.CacheStatus.SYNCED)); FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, actualCache, backendClientReturnsError); + firebaseApp, + firebaseInstallationsApi, + actualCache, + backendClientReturnsError, + taskExecutor); // Expect exception try { @@ -286,7 +310,11 @@ public void testClearCustomInstallationId_CacheOk_BackendError() throws Exceptio public void testClearCustomInstallationId_CacheError_BackendOk() throws InterruptedException { FirebaseSegmentation firebaseSegmentation = new FirebaseSegmentation( - firebaseApp, firebaseInstallationsApi, cacheReturnsError, backendClientReturnsOk); + firebaseApp, + firebaseInstallationsApi, + cacheReturnsError, + backendClientReturnsOk, + taskExecutor); // Expect exception try { diff --git a/firebase-storage/CHANGELOG.md b/firebase-storage/CHANGELOG.md index 7ffa4afeff7..60b401de0dd 100644 --- a/firebase-storage/CHANGELOG.md +++ b/firebase-storage/CHANGELOG.md @@ -1,18 +1,14 @@ # Unreleased -# 20.0.3 -- [fixed] Fixed an issue that caused infinite number of retries with no exponential - backoff for `uploadChunk` - -# 20.2.0 -* [unchanged] Updated to accommodate the release of the updated - [firebase_storage_full] Kotlin extensions library. +# 20.1.0 +* [fixed] Fixed an issue that caused an infinite number of retries with no + exponential backoff for `uploadChunk()`. ## Kotlin The Kotlin extensions library transitively includes the updated - `firebase-storage` library. The Kotlin extensions library has the following - additional updates: +`firebase-storage` library. The Kotlin extensions library has the following +additional updates: * [feature] Firebase now supports Kotlin coroutines. With this release, we added diff --git a/firebase-storage/gradle.properties b/firebase-storage/gradle.properties index 2fcd97394ef..2485d5054e8 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=20.0.3 -latestReleasedVersion=20.0.2 +version=20.1.1 +latestReleasedVersion=20.1.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/Storage.kt b/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/Storage.kt index 915f5d6f85e..763731f86fd 100644 --- a/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/Storage.kt +++ b/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/Storage.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.callbackFlow /** Returns the [FirebaseStorage] instance of the default [FirebaseApp]. */ val Firebase.storage: FirebaseStorage - get() = FirebaseStorage.getInstance() + get() = FirebaseStorage.getInstance() /** Returns the [FirebaseStorage] instance for a custom storage bucket at [url]. */ fun Firebase.storage(url: String): FirebaseStorage = FirebaseStorage.getInstance(url) @@ -50,13 +50,13 @@ fun Firebase.storage(app: FirebaseApp): FirebaseStorage = FirebaseStorage.getIns /** Returns the [FirebaseStorage] instance of a given [FirebaseApp] and storage bucket [url]. */ fun Firebase.storage(app: FirebaseApp, url: String): FirebaseStorage = - FirebaseStorage.getInstance(app, url) + FirebaseStorage.getInstance(app, url) /** Returns a [StorageMetadata] object initialized using the [init] function. */ fun storageMetadata(init: StorageMetadata.Builder.() -> Unit): StorageMetadata { - val builder = StorageMetadata.Builder() - builder.init() - return builder.build() + val builder = StorageMetadata.Builder() + builder.init() + return builder.build() } /** @@ -151,44 +151,47 @@ operator fun ListResult.component3(): String? = pageToken * - When the flow completes the listeners will be removed. */ val .SnapshotBase> StorageTask.taskState: Flow> - get() = callbackFlow { - val progressListener = OnProgressListener { snapshot -> - StorageTaskScheduler.getInstance().scheduleCallback { - trySendBlocking(TaskState.InProgress(snapshot)) - } + get() = callbackFlow { + val progressListener = + OnProgressListener { snapshot -> + StorageTaskScheduler.getInstance().scheduleCallback { + trySendBlocking(TaskState.InProgress(snapshot)) } - val pauseListener = OnPausedListener { snapshot -> - StorageTaskScheduler.getInstance().scheduleCallback { - trySendBlocking(TaskState.Paused(snapshot)) - } + } + val pauseListener = + OnPausedListener { snapshot -> + StorageTaskScheduler.getInstance().scheduleCallback { + trySendBlocking(TaskState.Paused(snapshot)) } - - // Only used to close or cancel the Flows, doesn't send any values - val completionListener = OnCompleteListener { task -> - if (task.isSuccessful) { - close() - } else { - val exception = task.exception - cancel("Error getting the TaskState", exception) - } + } + + // Only used to close or cancel the Flows, doesn't send any values + val completionListener = + OnCompleteListener { task -> + if (task.isSuccessful) { + close() + } else { + val exception = task.exception + cancel("Error getting the TaskState", exception) } + } - addOnProgressListener(progressListener) - addOnPausedListener(pauseListener) - addOnCompleteListener(completionListener) + addOnProgressListener(progressListener) + addOnPausedListener(pauseListener) + addOnCompleteListener(completionListener) - awaitClose { - removeOnProgressListener(progressListener) - removeOnPausedListener(pauseListener) - removeOnCompleteListener(completionListener) - } + awaitClose { + removeOnProgressListener(progressListener) + removeOnPausedListener(pauseListener) + removeOnCompleteListener(completionListener) } + } internal const val LIBRARY_NAME: String = "fire-stg-ktx" /** @suppress */ @Keep class FirebaseStorageKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = - listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) + override fun getComponents(): List> = + listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) } diff --git a/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/TaskState.kt b/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/TaskState.kt index 8b8454c8efb..e9a0f8590f6 100644 --- a/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/TaskState.kt +++ b/firebase-storage/ktx/src/main/kotlin/com/google/firebase/storage/ktx/TaskState.kt @@ -1,16 +1,27 @@ +// Copyright 2022 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.storage.ktx -/** - * Used to emit events about the progress of storage tasks. - */ +/** Used to emit events about the progress of storage tasks. */ abstract class TaskState private constructor() { - /** - * Called periodically as data is transferred and can be used to populate an upload/download indicator. - */ - class InProgress(val snapshot: T) : TaskState() + /** + * Called periodically as data is transferred and can be used to populate an upload/download + * indicator. + */ + class InProgress(val snapshot: T) : TaskState() - /** - * Called any time the upload/download is paused. - */ - class Paused(val snapshot: T) : TaskState() + /** Called any time the upload/download is paused. */ + class Paused(val snapshot: T) : TaskState() } diff --git a/firebase-storage/ktx/src/test/kotlin/com/google/firebase/storage/ktx/StorageTest.kt b/firebase-storage/ktx/src/test/kotlin/com/google/firebase/storage/ktx/StorageTest.kt index 1f8f9a099d9..8ec6ce00bfc 100644 --- a/firebase-storage/ktx/src/test/kotlin/com/google/firebase/storage/ktx/StorageTest.kt +++ b/firebase-storage/ktx/src/test/kotlin/com/google/firebase/storage/ktx/StorageTest.kt @@ -45,137 +45,141 @@ const val API_KEY = "API_KEY" const val EXISTING_APP = "existing" abstract class BaseTestCase { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build() - ) - - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId("123") - .build(), - EXISTING_APP - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } + @Before + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build() + ) + + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId("123") + .build(), + EXISTING_APP + ) + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } } @RunWith(RobolectricTestRunner::class) class StorageTests : BaseTestCase() { - @Test - fun `storage should delegate to FirebaseStorage#getInstance()`() { - assertThat(Firebase.storage).isSameInstanceAs(FirebaseStorage.getInstance()) - } - - @Test - fun `FirebaseApp#storage should delegate to FirebaseStorage#getInstance(FirebaseApp)`() { - val app = Firebase.app(EXISTING_APP) - assertThat(Firebase.storage(app)).isSameInstanceAs(FirebaseStorage.getInstance(app)) - } - - @Test - fun `Firebase#storage should delegate to FirebaseStorage#getInstance(url)`() { - val url = "gs://valid.url" - assertThat(Firebase.storage(url)).isSameInstanceAs(FirebaseStorage.getInstance(url)) + @Test + fun `storage should delegate to FirebaseStorage#getInstance()`() { + assertThat(Firebase.storage).isSameInstanceAs(FirebaseStorage.getInstance()) + } + + @Test + fun `FirebaseApp#storage should delegate to FirebaseStorage#getInstance(FirebaseApp)`() { + val app = Firebase.app(EXISTING_APP) + assertThat(Firebase.storage(app)).isSameInstanceAs(FirebaseStorage.getInstance(app)) + } + + @Test + fun `Firebase#storage should delegate to FirebaseStorage#getInstance(url)`() { + val url = "gs://valid.url" + assertThat(Firebase.storage(url)).isSameInstanceAs(FirebaseStorage.getInstance(url)) + } + + @Test + fun `Firebase#storage should delegate to FirebaseStorage#getInstance(FirebaseApp, url)`() { + val app = Firebase.app(EXISTING_APP) + val url = "gs://valid.url" + assertThat(Firebase.storage(app, url)).isSameInstanceAs(FirebaseStorage.getInstance(app, url)) + } + + @Test + fun `storageMetadata type-safe builder extension works`() { + val storage = Firebase.storage + val metadata: StorageMetadata = storageMetadata { + contentLanguage = "en_us" + contentType = "text/html" + contentEncoding = "utf-8" + cacheControl = "no-cache" + contentDisposition = "attachment" } - @Test - fun `Firebase#storage should delegate to FirebaseStorage#getInstance(FirebaseApp, url)`() { - val app = Firebase.app(EXISTING_APP) - val url = "gs://valid.url" - assertThat(Firebase.storage(app, url)).isSameInstanceAs(FirebaseStorage.getInstance(app, url)) - } - - @Test - fun `storageMetadata type-safe builder extension works`() { - val storage = Firebase.storage - val metadata: StorageMetadata = storageMetadata { - contentLanguage = "en_us" - contentType = "text/html" - contentEncoding = "utf-8" - cacheControl = "no-cache" - contentDisposition = "attachment" + assertThat(metadata.getContentType()).isEqualTo("text/html") + assertThat(metadata.getCacheControl()).isEqualTo("no-cache") + } + + @Test + fun `ListResult destructuring declarations work`() { + val mockListResult = + KtxTestUtil.listResult(listOf(), listOf(), null) + + val (items, prefixes, pageToken) = mockListResult + assertThat(items).isSameInstanceAs(mockListResult.items) + assertThat(prefixes).isSameInstanceAs(mockListResult.prefixes) + assertThat(pageToken).isSameInstanceAs(mockListResult.pageToken) + } + + @Test + fun `UploadTask#TaskSnapshot destructuring declarations work`() { + val mockTaskSnapshot = Mockito.mock(UploadTask.TaskSnapshot::class.java) + `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) + `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) + `when`(mockTaskSnapshot.metadata) + .thenReturn( + storageMetadata { + contentType = "image/png" + contentEncoding = "utf-8" } - - assertThat(metadata.getContentType()).isEqualTo("text/html") - assertThat(metadata.getCacheControl()).isEqualTo("no-cache") - } - - @Test - fun `ListResult destructuring declarations work`() { - val mockListResult = KtxTestUtil.listResult(listOf(), listOf(), null) - - val (items, prefixes, pageToken) = mockListResult - assertThat(items).isSameInstanceAs(mockListResult.items) - assertThat(prefixes).isSameInstanceAs(mockListResult.prefixes) - assertThat(pageToken).isSameInstanceAs(mockListResult.pageToken) - } - - @Test - fun `UploadTask#TaskSnapshot destructuring declarations work`() { - val mockTaskSnapshot = Mockito.mock(UploadTask.TaskSnapshot::class.java) - `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) - `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) - `when`(mockTaskSnapshot.metadata).thenReturn(storageMetadata { - contentType = "image/png" - contentEncoding = "utf-8" - }) - `when`(mockTaskSnapshot.uploadSessionUri).thenReturn(Uri.parse("https://test.com")) - - val (bytesTransferred, totalByteCount, metadata, sessionUri) = mockTaskSnapshot - - assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) - assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) - assertThat(metadata).isSameInstanceAs(mockTaskSnapshot.metadata) - assertThat(sessionUri).isSameInstanceAs(mockTaskSnapshot.uploadSessionUri) - } - - @Test - fun `StreamDownloadTask#TaskSnapshot destructuring declarations work`() { - val mockTaskSnapshot = Mockito.mock(StreamDownloadTask.TaskSnapshot::class.java) - `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) - `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) - `when`(mockTaskSnapshot.stream).thenReturn(ByteArrayInputStream("test".toByteArray())) - - val (bytesTransferred, totalByteCount, stream) = mockTaskSnapshot - - assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) - assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) - assertThat(stream).isSameInstanceAs(mockTaskSnapshot.stream) - } - - @Test - fun `FileDownloadTask#TaskSnapshot destructuring declarations work`() { - val mockTaskSnapshot = Mockito.mock(FileDownloadTask.TaskSnapshot::class.java) - `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) - `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) - - val (bytesTransferred, totalByteCount) = mockTaskSnapshot - - assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) - assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) - } + ) + `when`(mockTaskSnapshot.uploadSessionUri).thenReturn(Uri.parse("https://test.com")) + + val (bytesTransferred, totalByteCount, metadata, sessionUri) = mockTaskSnapshot + + assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) + assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) + assertThat(metadata).isSameInstanceAs(mockTaskSnapshot.metadata) + assertThat(sessionUri).isSameInstanceAs(mockTaskSnapshot.uploadSessionUri) + } + + @Test + fun `StreamDownloadTask#TaskSnapshot destructuring declarations work`() { + val mockTaskSnapshot = Mockito.mock(StreamDownloadTask.TaskSnapshot::class.java) + `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) + `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) + `when`(mockTaskSnapshot.stream).thenReturn(ByteArrayInputStream("test".toByteArray())) + + val (bytesTransferred, totalByteCount, stream) = mockTaskSnapshot + + assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) + assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) + assertThat(stream).isSameInstanceAs(mockTaskSnapshot.stream) + } + + @Test + fun `FileDownloadTask#TaskSnapshot destructuring declarations work`() { + val mockTaskSnapshot = Mockito.mock(FileDownloadTask.TaskSnapshot::class.java) + `when`(mockTaskSnapshot.bytesTransferred).thenReturn(50) + `when`(mockTaskSnapshot.totalByteCount).thenReturn(100) + + val (bytesTransferred, totalByteCount) = mockTaskSnapshot + + assertThat(bytesTransferred).isSameInstanceAs(mockTaskSnapshot.bytesTransferred) + assertThat(totalByteCount).isSameInstanceAs(mockTaskSnapshot.totalByteCount) + } } @RunWith(RobolectricTestRunner::class) class LibraryVersionTest : BaseTestCase() { - @Test - fun `library version should be registered with runtime`() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - assertThat(publisher.userAgent).contains(LIBRARY_NAME) - } + @Test + fun `library version should be registered with runtime`() { + val publisher = Firebase.app.get(UserAgentPublisher::class.java) + assertThat(publisher.userAgent).contains(LIBRARY_NAME) + } } diff --git a/firebase-storage/src/main/java/com/google/firebase/storage/StorageTask.java b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTask.java index 8aed4e1677e..c3d61c93ba4 100644 --- a/firebase-storage/src/main/java/com/google/firebase/storage/StorageTask.java +++ b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTask.java @@ -14,6 +14,7 @@ package com.google.firebase.storage; +import android.annotation.SuppressLint; import android.app.Activity; import android.util.Log; import androidx.annotation.NonNull; @@ -978,6 +979,8 @@ public Task onSuccessTask( return successTaskImpl(executor, continuation); } + // TODO(b/261014861): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull private Task continueWithTaskImpl( @Nullable final Executor executor, @@ -1020,6 +1023,8 @@ private Task continueWithTaskImpl( return source.getTask(); } + // TODO(b/261014861): Use an explicit executor in continuations. + @SuppressLint("TaskMainThread") @NonNull private Task successTaskImpl( @Nullable final Executor executor, diff --git a/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskScheduler.java b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskScheduler.java index d428ef3692f..1245e9668ea 100644 --- a/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskScheduler.java +++ b/firebase-storage/src/main/java/com/google/firebase/storage/StorageTaskScheduler.java @@ -14,6 +14,7 @@ package com.google.firebase.storage; +import android.annotation.SuppressLint; import androidx.annotation.NonNull; import androidx.annotation.RestrictTo; import java.util.concurrent.BlockingQueue; @@ -36,21 +37,33 @@ public class StorageTaskScheduler { public static StorageTaskScheduler sInstance = new StorageTaskScheduler(); private static BlockingQueue mCommandQueue = new LinkedBlockingQueue<>(); + + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static final ThreadPoolExecutor COMMAND_POOL_EXECUTOR = new ThreadPoolExecutor( 5, 5, 5, TimeUnit.SECONDS, mCommandQueue, new StorageThreadFactory("Command-")); private static BlockingQueue mUploadQueue = new LinkedBlockingQueue<>(); + + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static final ThreadPoolExecutor UPLOAD_QUEUE_EXECUTOR = new ThreadPoolExecutor( 2, 2, 5, TimeUnit.SECONDS, mUploadQueue, new StorageThreadFactory("Upload-")); private static BlockingQueue mDownloadQueue = new LinkedBlockingQueue<>(); + + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static final ThreadPoolExecutor DOWNLOAD_QUEUE_EXECUTOR = new ThreadPoolExecutor( 3, 3, 5, TimeUnit.SECONDS, mDownloadQueue, new StorageThreadFactory("Download-")); private static BlockingQueue mCallbackQueue = new LinkedBlockingQueue<>(); + + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") private static final ThreadPoolExecutor CALLBACK_QUEUE_EXECUTOR = new ThreadPoolExecutor( 1, 1, 5, TimeUnit.SECONDS, mCallbackQueue, new StorageThreadFactory("Callbacks-")); @@ -101,6 +114,8 @@ static class StorageThreadFactory implements ThreadFactory { @Override @SuppressWarnings("ThreadPriorityCheck") + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public Thread newThread(@NonNull Runnable r) { Thread t = new Thread(r, "FirebaseStorage-" + mNameSuffix + threadNumber.getAndIncrement()); t.setDaemon(false); diff --git a/firebase-storage/src/main/java/com/google/firebase/storage/internal/SmartHandler.java b/firebase-storage/src/main/java/com/google/firebase/storage/internal/SmartHandler.java index 1b3d4a7864d..d0cb5eeb5ee 100644 --- a/firebase-storage/src/main/java/com/google/firebase/storage/internal/SmartHandler.java +++ b/firebase-storage/src/main/java/com/google/firebase/storage/internal/SmartHandler.java @@ -14,6 +14,7 @@ package com.google.firebase.storage.internal; +import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; @@ -43,6 +44,8 @@ public class SmartHandler { /*package*/ static boolean testMode = false; /** Constructs a SmartHandler */ + // TODO(b/258426744): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") public SmartHandler(@Nullable Executor executor) { this.executor = executor; if (this.executor == null) { diff --git a/firebase-storage/src/testUtil/java/com/google/firebase/storage/network/MockInputStreamHelper.java b/firebase-storage/src/testUtil/java/com/google/firebase/storage/network/MockInputStreamHelper.java index e03c90575f8..c9f1ba566a1 100644 --- a/firebase-storage/src/testUtil/java/com/google/firebase/storage/network/MockInputStreamHelper.java +++ b/firebase-storage/src/testUtil/java/com/google/firebase/storage/network/MockInputStreamHelper.java @@ -14,6 +14,7 @@ package com.google.firebase.storage.network; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -37,6 +38,7 @@ public MockInputStreamHelper(final byte[] responseData) { this.injectExceptions = new TreeSet<>(); } + @CanIgnoreReturnValue public MockInputStreamHelper injectExceptionAt(int bytePos) { if (opened) { throw new IllegalStateException("Can't add exception points after reading from stream."); diff --git a/gradle.properties b/gradle.properties index 7508b2fb8ad..43b684aa426 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,6 @@ org.gradle.jvmargs=-Xmx8g -XX:MaxPermSize=8g org.gradle.parallel=true org.gradle.caching=true -firebase.checks.errorproneProjects=:tools:errorprone firebase.checks.lintProjects=:tools:lint systemProp.illegal-access=warn diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000000..f2f38812192 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,47 @@ +[versions] +android-lint = "30.3.1" +autovalue = "1.10.1" +coroutines = "1.6.4" +dagger = "2.43.2" +grpc = "1.50.2" +javalite = "3.17.3" +kotlin = "1.7.10" +protoc = "3.17.3" +robolectric = "4.9" +truth = "1.1.2" + +[libraries] +android-lint = { module = "com.android.tools.lint:lint", version.ref = "android-lint"} +android-lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "android-lint"} +android-lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "android-lint"} +android-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "android-lint"} +android-lint-testutils = { module = "com.android.tools:testutils", version.ref = "android-lint"} +androidx-annotation = { module = "androidx.annotation:annotation", version = "1.5.0" } +androidx-core = { module = "androidx.core:core", version = "1.2.0" } +androidx-futures = { module = "androidx.concurrent:concurrent-futures", version = "1.1.0" } +autovalue = { module = "com.google.auto.value:auto-value", version.ref = "autovalue" } +autovalue-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "autovalue" } +dagger-dagger = { module = "com.google.dagger:dagger", version.ref = "dagger"} +dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } +errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version = "2.9.0" } +findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2"} +javax-inject = { module = "javax.inject:javax.inject", version = "1" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } +okhttp = {module = "com.squareup.okhttp3:okhttp", version = "3.12.13"} +org-json = { module = "org.json:json", version = "20210307" } +playservices-base = { module = "com.google.android.gms:play-services-base", version = "18.1.0"} +playservices-basement = { module = "com.google.android.gms:play-services-basement", version = "18.1.0"} +playservices-tasks = { module = "com.google.android.gms:play-services-tasks", version = "18.0.2"} + +# Test libs +androidx-test-core = { module = "androidx.test:core", version = "1.5.0" } +androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.1.4" } +androidx-test-rules = { module = "androidx.test:rules", version = "1.5.0" } +androidx-test-runner = { module = "androidx.test:runner", version = "1.5.1" } +junit = { module = "junit:junit", version = "4.13.2" } +kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +mockito-core = { module = "org.mockito:mockito-core", version = "2.28.2"} +mockito-dexmaker = { module = "com.linkedin.dexmaker:dexmaker-mockito", version = "2.28.3"} +robolectric = {module = "org.robolectric:robolectric", version.ref = "robolectric" } +truth = { module = "com.google.truth:truth", version.ref = "truth"} diff --git a/gradle/projectSettings.gradle b/gradle/projectSettings.gradle index defb51e9fd9..69228b202d8 100644 --- a/gradle/projectSettings.gradle +++ b/gradle/projectSettings.gradle @@ -29,7 +29,9 @@ ext { /** Recursively renames build scripts to ${project.name}.gradle. */ renameBuildScripts = {ProjectDescriptor project -> - project.buildFileName = project.parent ? "${project.name}.gradle" : 'build.gradle' + def ktsFile = "${project.name}.gradle.kts" + def projectFile = new File(project.projectDir, ktsFile).exists() ? ktsFile : "${project.name}.gradle" + project.buildFileName = project.parent ? projectFile : 'build.gradle' project.children.each { renameBuildScripts(it) diff --git a/health-metrics/benchmark/README.md b/health-metrics/benchmark/README.md index 5cffaf1532a..3cd25e9617b 100644 --- a/health-metrics/benchmark/README.md +++ b/health-metrics/benchmark/README.md @@ -10,7 +10,7 @@ building a macrobenchmark test app for each of the Firebase Android SDKs. If not all of them are required, comment out irrelevant ones for faster build and test time. -## Run benchmark tests +## Run macrobenchmark tests ### Prerequisite @@ -35,16 +35,27 @@ and test time. [doc](https://cloud.google.com/docs/authentication) for full guidance on authentication. -### Run benchmark tests locally +### Run tests locally -1. Build all test apps by running below command in the root - directory `firebase-android-sdk`: +1. [Connect an Android device to the computer](https://d.android.com/studio/run/device) + +1. Run below command in the repository root directory `firebase-android-sdk`: ```shell - fireci macrobenchmark --build-only + fireci macrobenchmark run --local ``` -1. [Connect an Android device to the computer](https://d.android.com/studio/run/device) + **Note**: specify `--repeat ` to run the test multiple times. Run + `fireci macrobenchmark run --help` to see more details. + +Alternatively, developers can also create test apps with `fireci`, and run the +test from either CLI or Android Studio: + +1. Run below command to build all test apps: + + ```shell + fireci macrobenchmark run --build-only + ``` 1. Locate the temporary test apps directory from the log, for example: @@ -89,23 +100,90 @@ and test time. Alternatively, same set of result files are produced at the same output location as invoking tests from CLI, which can be used for inspection. -### Run benchmark tests on Firebase Test Lab +### Run tests on Firebase Test Lab -Build and run all tests on FTL by running below command in the root -directory `firebase-android-sdk`: +Run below command to build and run all tests on FTL: +```shell +fireci macrobenchmark run --remote ``` -fireci macrobenchmark -``` -Alternatively, it is possible to build all test apps via steps described in -[Running benchmark tests locally](#running-benchmark-tests-locally) -and manually -[run tests on FTL with `gcloud` CLI ](https://firebase.google.com/docs/test-lab/android/command-line#running_your_instrumentation_tests). +**Note**: `--repeat ` is also supported to submit the test to FTL for +`` times. All tests on FTL will run in parallel. + +Alternatively, developers can still build test apps locally, and manually +[run tests on FTL with `gcloud` CLI](https://firebase.google.com/docs/test-lab/android/command-line#running_your_instrumentation_tests). Aggregated benchmark results are displayed in the log. The log also contains links to FTL result pages and result files on Google Cloud Storage. +## Analyze macrobenchmark results + +Besides results from `*-benchmarkData.json` as descriped above, `fireci` +supports more in depth analysis, such as: + +- calculating percentiles and visualizing distributions for one test run +- comparing two sets of results (with stats and graphs) from two different runs + +To see more details, run + +```shell +fireci macrobenchmark analyze --help +``` + +### Example usage + +1. Analyzing local test results + + ```shell + fireci macrobenchmark analyze --local-reports-dir + ``` + + `` is the directory containing the `*-benchmarkData.json` from + the local test runs. + + **Note**: If the test is started: + + - with `fireci macrobenchmark run --local`, `fireci` copies all benchmark + json files into a dir, which can be supplied here. + - manually (CLI or Android Studio), `` shall be the directory + that contains `*-benchmarkData.json` in the gradle build directory. + +1. Analyzing remote test results + + ```shell + fireci macrobenchmark analyze --ftl-results-dir --ftl-results-dir ... + ``` + + ``, `` are Firebase Test Lab results directory names, such as + `2022-11-04_11:18:34.039437_OqZn`. + +1. Comparing two sets of result from two different FTL runs + + ```shell + fireci macrobenchmark analyze \ + --diff-mode \ + --ctl-ftl-results-dir \ + --ctl-ftl-results-dir \ + ... + --exp-ftl-results-dir \ + --exp-ftl-results-dir \ + ... + ``` + + `ctl` and `exp` are short for "control group" and "experimental group". + +1. Comparing a local test run against a FTL run + + ```shell + fireci macrobenchmark analyze \ + --diff-mode \ + --ctl-ftl-results-dir \ + --ctl-ftl-results-dir \ + ... + --exp-local-reports-dir + ``` + ## Toolchains - Gradle 7.5.1 diff --git a/health-metrics/benchmark/config.yaml b/health-metrics/benchmark/config.yaml index 8852965302e..8bf55d5a104 100644 --- a/health-metrics/benchmark/config.yaml +++ b/health-metrics/benchmark/config.yaml @@ -21,52 +21,42 @@ common-plugins: [com.google.gms.google-services] common-traces: [Firebase, ComponentDiscovery, Runtime] test-apps: - - sdk: firebase-config - name: config - dependencies: [com.google.firebase:firebase-config-ktx] - - sdk: firebase-common - name: common - dependencies: [com.google.firebase:firebase-common] - - sdk: firebase-crashlytics - name: crash - dependencies: [com.google.firebase:firebase-crashlytics-ktx] - plugins: [com.google.firebase.crashlytics] - - sdk: firebase-database - name: database - dependencies: [com.google.firebase:firebase-database-ktx] - - sdk: firebase-dynamic-links - name: fdl - dependencies: [com.google.firebase:firebase-dynamic-links-ktx] - - sdk: firebase-firestore - name: firestore - dependencies: [com.google.firebase:firebase-firestore-ktx] - - sdk: firebase-functions - name: functions - dependencies: [com.google.firebase:firebase-functions-ktx] - # TODO(yifany): disable temporarily due to errors of duplicate class and gradle crash - # - sdk: firebase-inappmessaging-display - # name: fiam - # dependencies: - # - com.google.firebase:firebase-analytics-ktx@18.0.3 - # - com.google.firebase:firebase-inappmessaging-ktx - # - com.google.firebase:firebase-inappmessaging-display-ktx - - sdk: firebase-messaging - name: message - dependencies: [com.google.firebase:firebase-messaging-ktx] - - sdk: firebase-perf - name: perf - dependencies: [com.google.firebase:firebase-perf-ktx] - plugins: [com.google.firebase.firebase-perf] - - sdk: firebase-storage - name: stroage - dependencies: [com.google.firebase:firebase-storage-ktx] - - -# TODO(yifany): google3 sdks, customizing FTL devices -# auth -# analytics -# combined -# - crashlytics + analytics -# - crashlytics + fireperf -# - auth + firestore -# - ... + - sdk: N.A. + name: all-included + dependencies: + - com.google.firebase:firebase-abt + - com.google.firebase:firebase-appcheck + - com.google.firebase:firebase-appdistribution + - com.google.firebase:firebase-crashlytics + - com.google.firebase:firebase-database + - com.google.firebase:firebase-dynamic-links + - com.google.firebase:firebase-firestore + - com.google.firebase:firebase-functions + - com.google.firebase:firebase-inappmessaging + - com.google.firebase:firebase-inappmessaging-display + - com.google.firebase:firebase-messaging + - com.google.firebase:firebase-ml-modeldownloader + - com.google.firebase:firebase-perf + - com.google.firebase:firebase-storage + plugins: + - com.google.firebase.crashlytics + - com.google.firebase.firebase-perf + traces: + - fire-abt + - fire-app-check + - fire-appdistribution + - fire-cls + - fire-dl + - fire-fcm + - fire-fiam + - fire-fiamd + - fire-fn + - fire-fst + - fire-gcs + - fire-installations + - firebase-ml-modeldownloader + - fire-perf + - fire-perf-early + - fire-rc + - fire-rtdb + - fire-transport diff --git a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/package-info.java b/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/Utilities.kt similarity index 77% rename from firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/package-info.java rename to health-metrics/benchmark/template/app/src/main/java/com/google/firebase/Utilities.kt index 23e006c76d4..14d840d9f3c 100644 --- a/firebase-messaging-directboot/src/main/java/com/google/firebase/messaging/directboot/threads/package-info.java +++ b/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/Utilities.kt @@ -12,5 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -/** @hide */ -package com.google.firebase.messaging.directboot.threads; + +package com.google.firebase + +import com.google.firebase.FirebaseApp + +internal fun initializeAllComponentsForBenchmark(app: FirebaseApp) { + app.initializeAllComponents() +} + diff --git a/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/benchmark/MainActivity.kt b/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/benchmark/MainActivity.kt index 0c23af1f54e..50043c2cf3d 100644 --- a/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/benchmark/MainActivity.kt +++ b/health-metrics/benchmark/template/app/src/main/java/com/google/firebase/benchmark/MainActivity.kt @@ -16,10 +16,15 @@ package com.google.firebase.benchmark import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import com.google.firebase.FirebaseApp +import com.google.firebase.initializeAllComponentsForBenchmark class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + val app = FirebaseApp.getInstance() + initializeAllComponentsForBenchmark(app) } } diff --git a/health-metrics/benchmark/template/macrobenchmark/src/main/java/com/google/firebase/macrobenchmark/BenchmarkTest.kt.mustache b/health-metrics/benchmark/template/macrobenchmark/src/main/java/com/google/firebase/macrobenchmark/BenchmarkTest.kt.mustache index 4fa5af3546e..82dd0ecbf3b 100644 --- a/health-metrics/benchmark/template/macrobenchmark/src/main/java/com/google/firebase/macrobenchmark/BenchmarkTest.kt.mustache +++ b/health-metrics/benchmark/template/macrobenchmark/src/main/java/com/google/firebase/macrobenchmark/BenchmarkTest.kt.mustache @@ -39,7 +39,7 @@ class StartupBenchmark { TraceSectionMetric("{{.}}"), {{/traces}} ), - iterations = 5, + iterations = 100, startupMode = StartupMode.COLD ) { pressHome() diff --git a/integ-testing/integ-testing.gradle b/integ-testing/integ-testing.gradle index db10ae7d882..1da35db0c17 100644 --- a/integ-testing/integ-testing.gradle +++ b/integ-testing/integ-testing.gradle @@ -30,6 +30,8 @@ android { } dependencies { + implementation project(":firebase-common") + implementation project(":firebase-components") implementation 'junit:junit:4.13' implementation 'androidx.test:runner:1.3.0' } diff --git a/integ-testing/src/main/java/com/google/firebase/concurrent/TestOnlyExecutors.java b/integ-testing/src/main/java/com/google/firebase/concurrent/TestOnlyExecutors.java new file mode 100644 index 00000000000..84ee68afe60 --- /dev/null +++ b/integ-testing/src/main/java/com/google/firebase/concurrent/TestOnlyExecutors.java @@ -0,0 +1,39 @@ +// Copyright 2022 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.concurrent; + +import androidx.annotation.RestrictTo; + +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; + +@RestrictTo(RestrictTo.Scope.TESTS) +public class TestOnlyExecutors { + public static Executor ui() { + return UiExecutor.INSTANCE; + } + + public static ScheduledExecutorService background() { + return ExecutorsRegistrar.BG_EXECUTOR.get(); + } + + public static ScheduledExecutorService blocking() { + return ExecutorsRegistrar.BLOCKING_EXECUTOR.get(); + } + + public static ScheduledExecutorService lite() { + return ExecutorsRegistrar.LITE_EXECUTOR.get(); + } +} diff --git a/kotlindoc/package-lists/android/package-list b/kotlindoc/package-lists/android/package-list index 9b7202c46b8..b8d9fe5ff35 100644 --- a/kotlindoc/package-lists/android/package-list +++ b/kotlindoc/package-lists/android/package-list @@ -3,6 +3,7 @@ android.accessibilityservice android.accounts android.animation android.annotation +androidx.annotation android.app android.app.admin android.app.assist diff --git a/kotlindoc/package-lists/firebase/package-list b/kotlindoc/package-lists/firebase/package-list deleted file mode 100644 index 0b11ecaccf7..00000000000 --- a/kotlindoc/package-lists/firebase/package-list +++ /dev/null @@ -1,44 +0,0 @@ -com.google.firebase -com.google.firebase.analytics -com.google.firebase.analytics.ktx -com.google.firebase.appcheck -com.google.firebase.appcheck.ktx -com.google.firebase.appcheck.debug -com.google.firebase.appcheck.debug.testing -com.google.firebase.appcheck.playintegrity -com.google.firebase.appcheck.safetynet -com.google.firebase.appdistribution -com.google.firebase.appdistribution.ktx -com.google.firebase.auth -com.google.firebase.auth.ktx -com.google.firebase.crashlytics -com.google.firebase.crashlytics.ktx -com.google.firebase.database -com.google.firebase.database.ktx -com.google.firebase.dynamiclinks -com.google.firebase.dynamiclinks.ktx -com.google.firebase.firestore -com.google.firebase.firestore.ktx -com.google.firebase.functions -com.google.firebase.functions.ktx -com.google.firebase.inappmessaging -com.google.firebase.inappmessaging.display -com.google.firebase.inappmessaging.display.ktx -com.google.firebase.inappmessaging.ktx -com.google.firebase.inappmessaging.model -com.google.firebase.installations -com.google.firebase.installations.ktx -com.google.firebase.ktx -com.google.firebase.messaging -com.google.firebase.messaging.ktx -com.google.firebase.ml -com.google.firebase.ml.modeldownloader -com.google.firebase.ml.modeldownloader.ktx -com.google.firebase.perf -com.google.firebase.perf.ktx -com.google.firebase.perf.metrics -com.google.firebase.provider -com.google.firebase.remoteconfig -com.google.firebase.remoteconfig.ktx -com.google.firebase.storage -com.google.firebase.storage.ktx diff --git a/settings.gradle b/settings.gradle index b52d04ceb36..568350d0769 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,8 @@ rootProject.name = 'com.google.firebase' +enableFeaturePreview("VERSION_CATALOGS") + //Note: do not add subprojects to this file. Instead add them to subprojects.gradle apply from: 'gradle/projectSettings.gradle' diff --git a/subprojects.cfg b/subprojects.cfg index 1da5e643526..00c46f72e7b 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -67,7 +67,6 @@ encoders:protoc-gen-firebase-encoders:tests integ-testing -tools:errorprone tools:lint transport diff --git a/tools/errorprone/src/main/java/com/google/firebase/errorprone/ComponentsAppGetCheck.java b/tools/errorprone/src/main/java/com/google/firebase/errorprone/ComponentsAppGetCheck.java deleted file mode 100644 index b42c5dbfa48..00000000000 --- a/tools/errorprone/src/main/java/com/google/firebase/errorprone/ComponentsAppGetCheck.java +++ /dev/null @@ -1,146 +0,0 @@ -// 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.errorprone; - -import static com.google.errorprone.matchers.Matchers.allOf; -import static com.google.errorprone.matchers.Matchers.anyOf; -import static com.google.errorprone.matchers.Matchers.enclosingClass; -import static com.google.errorprone.matchers.Matchers.enclosingMethod; -import static com.google.errorprone.matchers.Matchers.hasAnnotation; -import static com.google.errorprone.matchers.Matchers.instanceMethod; -import static com.google.errorprone.matchers.Matchers.isStatic; -import static com.google.errorprone.matchers.Matchers.methodHasVisibility; -import static com.google.errorprone.matchers.Matchers.methodInvocation; -import static com.google.errorprone.matchers.Matchers.methodIsNamed; -import static com.google.errorprone.matchers.Matchers.methodReturns; -import static com.google.errorprone.util.ASTHelpers.getType; -import static com.google.errorprone.util.ASTHelpers.isSubtype; - -import com.google.auto.service.AutoService; -import com.google.errorprone.BugPattern; -import com.google.errorprone.VisitorState; -import com.google.errorprone.bugpatterns.BugChecker; -import com.google.errorprone.matchers.AbstractTypeMatcher; -import com.google.errorprone.matchers.Description; -import com.google.errorprone.matchers.Matcher; -import com.google.errorprone.matchers.MethodVisibility; -import com.google.errorprone.suppliers.Supplier; -import com.google.errorprone.util.ASTHelpers; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; -import com.sun.tools.javac.code.Type; - -/** - * Errorprone custom check that discourages use of FirebaseApp#get(Class) as it is only intended for - * use in Sdk#getInstance() methods. - */ -@BugPattern( - name = "FirebaseUseExplicitDependencies", - summary = - "Use of FirebaseApp#get(Class) is discouraged, and is only acceptable" - + " in SDK#getInstance(...) methods. Instead declare dependencies explicitly in" - + " your ComponentRegistrar and inject.", - severity = BugPattern.SeverityLevel.ERROR) -@AutoService(BugChecker.class) -public class ComponentsAppGetCheck extends BugChecker - implements BugChecker.MethodInvocationTreeMatcher { - private static final String FIREBASE_APP = "com.google.firebase.FirebaseApp"; - private static final String GET_COMPONENT_METHOD = "get(java.lang.Class)"; - - private static final Matcher CALL_TO_GET = - methodInvocation( - instanceMethod().onExactClass(FIREBASE_APP).withSignature(GET_COMPONENT_METHOD)); - - /** - * This matches methods of the forms: - * - *

{@code
-   * class Foo {
-   *     public static Foo getInstance(/* any number of parameters * /);
-   * }
-   *
-   * class Foo extends/implements Bar {
-   *     public static Bar getInstance(/* any number of parameters * /);
-   * }
-   * }
- */ - private static final Matcher WITHIN_GET_INSTANCE = - enclosingMethod( - allOf( - isStatic(), - methodIsNamed("getInstance"), - methodReturns( - isSupertypeOf( - state -> ASTHelpers.getType(state.findEnclosing(ClassTree.class)))))); - - /** - * This matches methods of the forms: - * - *
{@code
-   * class Foo {
-   *     private static Foo getInstanceImpl(/* any number of parameters * /);
-   * }
-   *
-   * class Foo extends/implements Bar {
-   *     private static Bar getInstanceImpl(/* any number of parameters * /);
-   * }
-   * }
- */ - private static final Matcher WITHIN_GET_INSTANCE_IMPL = - enclosingMethod( - allOf( - isStatic(), - methodHasVisibility(MethodVisibility.Visibility.PRIVATE), - methodIsNamed("getInstanceImpl"), - methodReturns( - isSupertypeOf( - state -> ASTHelpers.getType(state.findEnclosing(ClassTree.class)))))); - - private static final Matcher WITHIN_JUNIT_TEST = - enclosingClass(hasAnnotation("org.junit.runner.RunWith")); - - private static final Matcher ALLOWED_USAGES = - anyOf(WITHIN_GET_INSTANCE, WITHIN_GET_INSTANCE_IMPL, WITHIN_JUNIT_TEST); - - @Override - public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { - if (ALLOWED_USAGES.matches(tree, state) || !CALL_TO_GET.matches(tree, state)) { - return Description.NO_MATCH; - } - return describeMatch(tree); - } - - private static Matcher isSupertypeOf(Supplier type) { - return new IsSupertypeOf(type); - } - - private static class IsSupertypeOf extends AbstractTypeMatcher { - - public IsSupertypeOf(Supplier typeToCompareSupplier) { - super(typeToCompareSupplier); - } - - public IsSupertypeOf(String typeString) { - super(typeString); - } - - @Override - public boolean matches(T tree, VisitorState state) { - return isSubtype(typeToCompareSupplier.get(state), getType(tree), state); - } - } -} diff --git a/tools/errorprone/src/test/java/com/google/firebase/errorprone/ComponentsAppGetCheckTest.java b/tools/errorprone/src/test/java/com/google/firebase/errorprone/ComponentsAppGetCheckTest.java deleted file mode 100644 index e50b758f1e3..00000000000 --- a/tools/errorprone/src/test/java/com/google/firebase/errorprone/ComponentsAppGetCheckTest.java +++ /dev/null @@ -1,42 +0,0 @@ -// 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.errorprone; - -import com.google.errorprone.CompilationTestHelper; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ComponentsAppGetCheckTest { - - private CompilationTestHelper compilationHelper; - - @Before - public void setup() { - compilationHelper = CompilationTestHelper.newInstance(ComponentsAppGetCheck.class, getClass()); - } - - @Test - public void testPositiveCases() { - compilationHelper.addSourceFile("FirebaseUseExplicitDependenciesPositiveCases.java").doTest(); - } - - @Test - public void testNegativeCases() { - compilationHelper.addSourceFile("FirebaseUseExplicitDependenciesNegativeCases.java").doTest(); - } -} diff --git a/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesNegativeCases.java b/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesNegativeCases.java deleted file mode 100644 index 009b61ab43d..00000000000 --- a/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesNegativeCases.java +++ /dev/null @@ -1,74 +0,0 @@ -// 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.errorprone; - -import com.google.firebase.FirebaseApp; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -public class FirebaseUseExplicitDependenciesNegativeCases { - - /** Valid use with one app parameter. */ - public static FirebaseUseExplicitDependenciesNegativeCases getInstance(FirebaseApp app) { - app.get(String.class); - return null; - } - - /** Valid use with multiple parameters. */ - public static FirebaseUseExplicitDependenciesNegativeCases getInstance( - FirebaseApp app, String foo) { - app.get(String.class); - return null; - } - - /** Valid use with no parameters. */ - public static FirebaseUseExplicitDependenciesNegativeCases getInstance() { - new FirebaseApp().get(String.class); - return null; - } - - /** Valid private use with multiple parameters. */ - private static FirebaseUseExplicitDependenciesNegativeCases getInstance( - FirebaseApp app, Integer i) { - app.get(String.class); - return null; - } - - /** Use allowed in tests. */ - @RunWith(JUnit4.class) - private static class MyTest { - public void test() { - new FirebaseApp().get(String.class); - } - } - - public static class SuperType {} - - public static class SubType extends SuperType { - public static SuperType getInstance(FirebaseApp app) { - app.get(String.class); - return null; - } - } - - public interface Iface {} - - public static class IfaceImpl implements Iface { - public static Iface getInstance(FirebaseApp app) { - app.get(String.class); - return null; - } - } -} diff --git a/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesPositiveCases.java b/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesPositiveCases.java deleted file mode 100644 index 800136f82d3..00000000000 --- a/tools/errorprone/src/test/resources/com/google/firebase/errorprone/FirebaseUseExplicitDependenciesPositiveCases.java +++ /dev/null @@ -1,51 +0,0 @@ -// 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.errorprone; - -import com.google.firebase.FirebaseApp; - -public class FirebaseUseExplicitDependenciesPositiveCases { - private FirebaseApp app = new FirebaseApp(); - - public void useOfAppGetInInstanceMethod(FirebaseApp app) { - // BUG: Diagnostic contains: FirebaseApp#get(Class) is discouraged - app.get(String.class); - } - - public void useOfAppGetInStaticMethod() { - // BUG: Diagnostic contains: FirebaseApp#get(Class) is discouraged - app.get(String.class); - } - - /** method returns void so it is not allowed. */ - public static void getInstance(FirebaseApp app, String foo) { - // BUG: Diagnostic contains: FirebaseApp#get(Class) is discouraged - app.get(String.class); - } - - /** method returns int so it is not allowed. */ - public static int getInstance(String foo) { - // BUG: Diagnostic contains: FirebaseApp#get(Class) is discouraged - new FirebaseApp().get(String.class); - return 0; - } - - /** method returns String so it is not allowed. */ - public static String getInstance(FirebaseApp app) { - // BUG: Diagnostic contains: FirebaseApp#get(Class) is discouraged - app.get(String.class); - return ""; - } -} diff --git a/tools/lint/lint.gradle b/tools/lint/lint.gradle deleted file mode 100644 index d1685fbc110..00000000000 --- a/tools/lint/lint.gradle +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'org.jetbrains.kotlin.jvm' -} - -def lintVersion = '30.2.2' - -dependencies { - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.lint:lint-checks:$lintVersion" - - testImplementation "junit:junit:4.12" - testImplementation ("com.android.tools.lint:lint:$lintVersion") { - exclude group: "com.google.protobuf", module: "protobuf-java" - } - testImplementation ("com.android.tools.lint:lint-tests:$lintVersion") { - exclude group: "com.google.protobuf", module: "protobuf-java" - } - testImplementation "com.android.tools:testutils:$lintVersion" -} - -jar { - manifest { - attributes('Lint-Registry-v2': 'com.google.firebase.lint.checks.CheckRegistry') - } -} diff --git a/tools/errorprone/errorprone.gradle b/tools/lint/lint.gradle.kts similarity index 55% rename from tools/errorprone/errorprone.gradle rename to tools/lint/lint.gradle.kts index f8be3518dbf..9c8cda3df19 100644 --- a/tools/errorprone/errorprone.gradle +++ b/tools/lint/lint.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2018 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,15 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -apply plugin: 'java-library' - +plugins { + id("org.jetbrains.kotlin.jvm") +} dependencies { - implementation 'com.google.errorprone:error_prone_check_api:2.3.2' - implementation 'com.google.auto.service:auto-service:1.0-rc4' - annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4' + compileOnly(libs.kotlin.stdlib) + compileOnly(libs.android.lint.api) + compileOnly(libs.android.lint.checks) + + testImplementation(libs.junit) + testImplementation(libs.android.lint) + testImplementation(libs.android.lint.tests) + testImplementation(libs.android.lint.testutils) +} - testImplementation 'junit:junit:4.12' - testImplementation 'com.google.errorprone:error_prone_test_helpers:2.3.3' - testImplementation 'com.google.errorprone:javac:9+181-r4173-1' +tasks.jar { + manifest { + attributes("Lint-Registry-v2" to "com.google.firebase.lint.checks.CheckRegistry") + } } diff --git a/tools/lint/src/main/kotlin/Annotations.kt b/tools/lint/src/main/kotlin/Annotations.kt index e69509e4486..d6bee6d2027 100644 --- a/tools/lint/src/main/kotlin/Annotations.kt +++ b/tools/lint/src/main/kotlin/Annotations.kt @@ -27,60 +27,68 @@ import org.jetbrains.uast.getParentOfType internal const val ANNOTATION = "com.google.firebase.annotations.DeferredApi" fun hasDeferredApiAnnotation(context: JavaContext, methodCall: UElement): Boolean { - lambdaMethod(methodCall)?.let { - return hasDeferredApiAnnotation(context, it) - } + lambdaMethod(methodCall)?.let { + return hasDeferredApiAnnotation(context, it) + } - val method = methodCall.getParentOfType( - UMethod::class.java, true, - UAnonymousClass::class.java, ULambdaExpression::class.java + val method = + methodCall.getParentOfType( + UMethod::class.java, + true, + UAnonymousClass::class.java, + ULambdaExpression::class.java ) as? PsiMethod - return hasDeferredApiAnnotation(context, method) + return hasDeferredApiAnnotation(context, method) } fun hasDeferredApiAnnotation(context: JavaContext, calledMethod: PsiMethod?): Boolean { - var method = calledMethod ?: return false + var method = calledMethod ?: return false - while (true) { - for (annotation in method.modifierList.annotations) { - annotation.qualifiedName?.let { - if (it == ANNOTATION) { - return@hasDeferredApiAnnotation true - } - } + while (true) { + for (annotation in method.modifierList.annotations) { + annotation.qualifiedName?.let { + if (it == ANNOTATION) { + return@hasDeferredApiAnnotation true } - method = context.evaluator.getSuperMethod(method) ?: break + } } + method = context.evaluator.getSuperMethod(method) ?: break + } - var cls = method.containingClass ?: return false + var cls = method.containingClass ?: return false - while (true) { - val modifierList = cls.modifierList - if (modifierList != null) { - for (annotation in modifierList.annotations) { - annotation.qualifiedName?.let { - if (it == ANNOTATION) { - return@hasDeferredApiAnnotation true - } - } - } + while (true) { + val modifierList = cls.modifierList + if (modifierList != null) { + for (annotation in modifierList.annotations) { + annotation.qualifiedName?.let { + if (it == ANNOTATION) { + return@hasDeferredApiAnnotation true + } } - cls = cls.superClass ?: break + } } - return false + cls = cls.superClass ?: break + } + return false } fun lambdaMethod(element: UElement): PsiMethod? { - val lambda = element.getParentOfType( - ULambdaExpression::class.java, true, UMethod::class.java, UAnonymousClass::class.java) - ?: return null + val lambda = + element.getParentOfType( + ULambdaExpression::class.java, + true, + UMethod::class.java, + UAnonymousClass::class.java + ) + ?: return null - val type = lambda.functionalInterfaceType - if (type is PsiClassType) { - val resolved = type.resolve() - if (resolved != null) { - return resolved.allMethods.firstOrNull { it.hasModifier(JvmModifier.ABSTRACT) } - } + val type = lambda.functionalInterfaceType + if (type is PsiClassType) { + val resolved = type.resolve() + if (resolved != null) { + return resolved.allMethods.firstOrNull { it.hasModifier(JvmModifier.ABSTRACT) } } - return null + } + return null } diff --git a/tools/lint/src/main/kotlin/CheckRegistry.kt b/tools/lint/src/main/kotlin/CheckRegistry.kt index ebd68916a9f..a3263522b16 100644 --- a/tools/lint/src/main/kotlin/CheckRegistry.kt +++ b/tools/lint/src/main/kotlin/CheckRegistry.kt @@ -15,24 +15,37 @@ package com.google.firebase.lint.checks import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API import com.android.tools.lint.detector.api.Issue class CheckRegistry : IssueRegistry() { - override val issues: List - get() = listOf( - ManifestElementHasNoExportedAttributeDetector.EXPORTED_MISSING_ISSUE, - KotlinInteropDetector.KOTLIN_PROPERTY, - KotlinInteropDetector.LAMBDA_LAST, - KotlinInteropDetector.NO_HARD_KOTLIN_KEYWORDS, - KotlinInteropDetector.PLATFORM_NULLNESS, - NonAndroidxNullabilityDetector.NON_ANDROIDX_NULLABILITY, - DeferredApiDetector.INVALID_DEFERRED_API_USE, - ProviderAssignmentDetector.INVALID_PROVIDER_ASSIGNMENT - ) + override val issues: List + get() = + listOf( + ManifestElementHasNoExportedAttributeDetector.EXPORTED_MISSING_ISSUE, + KotlinInteropDetector.KOTLIN_PROPERTY, + KotlinInteropDetector.LAMBDA_LAST, + KotlinInteropDetector.NO_HARD_KOTLIN_KEYWORDS, + KotlinInteropDetector.PLATFORM_NULLNESS, + NonAndroidxNullabilityDetector.NON_ANDROIDX_NULLABILITY, + DeferredApiDetector.INVALID_DEFERRED_API_USE, + ProviderAssignmentDetector.INVALID_PROVIDER_ASSIGNMENT, + ThreadPoolDetector.THREAD_POOL_CREATION, + TasksMainThreadDetector.TASK_MAIN_THREAD, + FirebaseAppGetDetector.ISSUE, + ) - override val api: Int - get() = CURRENT_API - override val minApi: Int - get() = 2 + override val api: Int + get() = CURRENT_API + override val minApi: Int + get() = 2 + + override val vendor: Vendor + get() = + Vendor( + "firebase", + identifier = "firebase", + feedbackUrl = "https://github.com/firebase/firebase-android-sdk/issues" + ) } diff --git a/tools/lint/src/main/kotlin/DeferredApiDetector.kt b/tools/lint/src/main/kotlin/DeferredApiDetector.kt index dc4c56efcc5..ac9746f9f16 100644 --- a/tools/lint/src/main/kotlin/DeferredApiDetector.kt +++ b/tools/lint/src/main/kotlin/DeferredApiDetector.kt @@ -14,6 +14,9 @@ package com.google.firebase.lint.checks +import com.android.tools.lint.detector.api.AnnotationInfo +import com.android.tools.lint.detector.api.AnnotationOrigin +import com.android.tools.lint.detector.api.AnnotationUsageInfo import com.android.tools.lint.detector.api.AnnotationUsageType import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector @@ -24,67 +27,63 @@ import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiMethod -import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement +@Suppress("DetectorIsMissingAnnotations") class DeferredApiDetector : Detector(), SourceCodeScanner { - override fun applicableAnnotations(): List = listOf(ANNOTATION) + override fun applicableAnnotations(): List = listOf(ANNOTATION) - override fun visitAnnotationUsage( - context: JavaContext, - usage: UElement, - type: AnnotationUsageType, - annotation: UAnnotation, - qualifiedName: String, - method: PsiMethod?, - annotations: List, - allMemberAnnotations: List, - allClassAnnotations: List, - allPackageAnnotations: List + override fun visitAnnotationUsage( + context: JavaContext, + element: UElement, + annotationInfo: AnnotationInfo, + usageInfo: AnnotationUsageInfo + ) { + if ( + usageInfo.type == AnnotationUsageType.METHOD_CALL && + annotationInfo.origin == AnnotationOrigin.METHOD ) { - if (method != null && type == AnnotationUsageType.METHOD_CALL) { - check(context, usage as UCallExpression, method) - } + check(context, usageInfo.usage as UCallExpression, annotationInfo.annotated as PsiMethod) } + } - private fun check(context: JavaContext, usage: UCallExpression, method: PsiMethod) { - val usageHasAnnotation = hasDeferredApiAnnotation(context, usage) - val methodHasAnnotation = hasDeferredApiAnnotation(context, method) + private fun check(context: JavaContext, usage: UCallExpression, method: PsiMethod) { + val usageHasAnnotation = hasDeferredApiAnnotation(context, usage) + val methodHasAnnotation = hasDeferredApiAnnotation(context, method) - if ((!usageHasAnnotation && methodHasAnnotation) || (usageHasAnnotation && !methodHasAnnotation)) - context.report( - INVALID_DEFERRED_API_USE, - usage, - context.getCallLocation( - usage, - includeReceiver = false, - includeArguments = true), - "${method.name} is only safe to call in the context of a Deferred dependency.") - } - - companion object { - private val IMPLEMENTATION = Implementation( - DeferredApiDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) + if ( + (!usageHasAnnotation && methodHasAnnotation) || (usageHasAnnotation && !methodHasAnnotation) + ) + context.report( + INVALID_DEFERRED_API_USE, + usage, + context.getCallLocation(usage, includeReceiver = false, includeArguments = true), + "${method.name} is only safe to call in the context of a Deferred`` dependency" + ) + } - /** Calling methods on the wrong thread */ - @JvmField - val INVALID_DEFERRED_API_USE = Issue.create( - id = "InvalidDeferredApiUse", - briefDescription = "Invalid use of @DeferredApi", + companion object { + private val IMPLEMENTATION = + Implementation(DeferredApiDetector::class.java, Scope.JAVA_FILE_SCOPE) - explanation = """ - Ensures that a method which expects to be called in the context of - Deferred#whenAvailable(), is actually called this way. This is important for - supporting dynamically loaded modules, where certain dependencies become available + /** Calling methods on the wrong thread */ + @JvmField + val INVALID_DEFERRED_API_USE = + Issue.create( + id = "InvalidDeferredApiUse", + briefDescription = "Invalid use of @DeferredApi", + explanation = + """ + Ensures that a method which expects to be called in the context of \ + `Deferred#whenAvailable()`, is actually called this way. This is important for \ + supporting dynamically loaded modules, where certain dependencies become available \ during app's runtime and not available upon app launch. """, - category = Category.CORRECTNESS, - priority = 6, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - } + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } } diff --git a/tools/lint/src/main/kotlin/FirebaseAppGetDetector.kt b/tools/lint/src/main/kotlin/FirebaseAppGetDetector.kt new file mode 100644 index 00000000000..c6f20b0a235 --- /dev/null +++ b/tools/lint/src/main/kotlin/FirebaseAppGetDetector.kt @@ -0,0 +1,95 @@ +// Copyright 2022 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.lint.checks + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.InheritanceUtil +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.getParentOfType + +@Suppress("DetectorIsMissingAnnotations") +class FirebaseAppGetDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List = listOf("get") + + override fun visitMethodCall(context: JavaContext, call: UCallExpression, method: PsiMethod) { + if (!isFirebaseAppGet(method)) { + return + } + + if (withinGetInstance(call)) { + return + } + call.getParentOfType() ?: return + context.report( + ISSUE, + call, + context.getCallLocation(call, includeReceiver = false, includeArguments = true), + "Use of FirebaseApp#get(Class) is discouraged, and is only acceptable" + + " in SDK#getInstance(...) methods. Instead declare dependencies explicitly in" + + " your ComponentRegistrar and inject." + ) + } + + private fun withinGetInstance(call: UCallExpression): Boolean { + val withinMethod = call.getParentOfType() ?: return false + if (withinMethod.name != "getInstance" && !withinMethod.isStatic) return false + + var containingClass: PsiClass = withinMethod.containingClass ?: return false + if (containingClass.name == "Companion") { + containingClass = containingClass.containingClass ?: return false + } + return InheritanceUtil.isInheritor( + withinMethod.returnType, + containingClass.qualifiedName ?: return false + ) + } + + private fun isFirebaseAppGet(method: PsiMethod): Boolean { + val cls = (method.parent as? PsiClass) ?: return false + return cls.qualifiedName == "com.google.firebase.FirebaseApp" && + method.parameterList.parametersCount == 1 + } + + companion object { + private val IMPLEMENTATION = + Implementation(FirebaseAppGetDetector::class.java, Scope.JAVA_FILE_SCOPE) + val ISSUE = + Issue.create( + "FirebaseUseExplicitDependencies", + briefDescription = + "Use of FirebaseApp#get(Class) is discouraged, and is only acceptable" + + " in SDK#getInstance(...) methods. Instead declare dependencies explicitly in" + + " your ComponentRegistrar and inject.", + explanation = + "Use of FirebaseApp#get(Class) is discouraged, and is only acceptable" + + " in SDK#getInstance(...) methods. Instead declare dependencies explicitly in" + + " your ComponentRegistrar and inject.", + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } +} diff --git a/tools/lint/src/main/kotlin/KotlinInteropDetector.kt b/tools/lint/src/main/kotlin/KotlinInteropDetector.kt index 4476ea75b5d..a9d475d8785 100644 --- a/tools/lint/src/main/kotlin/KotlinInteropDetector.kt +++ b/tools/lint/src/main/kotlin/KotlinInteropDetector.kt @@ -52,676 +52,696 @@ import org.jetbrains.uast.getContainingUClass import org.jetbrains.uast.getContainingUMethod import org.jetbrains.uast.getParentOfType +@Suppress("DetectorIsMissingAnnotations") class KotlinInteropDetector : Detector(), SourceCodeScanner { - companion object Issues { - private val IMPLEMENTATION = Implementation( - KotlinInteropDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - - const val IGNORE_DEPRECATED = false - - @JvmField - val NO_HARD_KOTLIN_KEYWORDS = Issue.create( - id = "FirebaseNoHardKeywords", - briefDescription = "No Hard Kotlin Keywords", - - explanation = """ - Do not use Kotlin’s hard keywords as the name of methods or fields. - These require the use of backticks to escape when calling from Kotlin. + companion object Issues { + private val IMPLEMENTATION = + Implementation(KotlinInteropDetector::class.java, Scope.JAVA_FILE_SCOPE) + + const val IGNORE_DEPRECATED = false + + @JvmField + val NO_HARD_KOTLIN_KEYWORDS = + Issue.create( + id = "FirebaseNoHardKeywords", + briefDescription = "No Hard Kotlin Keywords", + explanation = + """ + Do not use Kotlin’s hard keywords as the name of methods or fields. \ + These require the use of backticks to escape when calling from Kotlin. \ Soft keywords, modifier keywords, and special identifiers are allowed. For example, Mockito’s `when` function requires backticks when used from Kotlin: - val callable = Mockito.mock(Callable::class.java) + val callable = Mockito.mock(Callable::class.java) \ Mockito.\`when\`(callable.call()).thenReturn(/* … */) """, - category = Category.INTEROPERABILITY_KOTLIN, - priority = 1, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - - @JvmField - val LAMBDA_LAST = Issue.create( - id = "FirebaseLambdaLast", - briefDescription = "Lambda Parameters Last", - - explanation = """ - To improve calling this code from Kotlin, + category = Category.INTEROPERABILITY_KOTLIN, + priority = 1, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + + @JvmField + val LAMBDA_LAST = + Issue.create( + id = "FirebaseLambdaLast", + briefDescription = "Lambda Parameters Last", + explanation = + """ + To improve calling this code from Kotlin, \ parameter types eligible for SAM conversion should be last. """, - category = Category.INTEROPERABILITY_KOTLIN, - priority = 1, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - - @JvmField - val PLATFORM_NULLNESS = Issue.create( - id = "FirebaseUnknownNullness", - briefDescription = "Unknown nullness", - - explanation = """ - To improve referencing this code from Kotlin, consider adding + category = Category.INTEROPERABILITY_KOTLIN, + priority = 1, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + + @JvmField + val PLATFORM_NULLNESS = + Issue.create( + id = "FirebaseUnknownNullness", + briefDescription = "Unknown nullness", + explanation = + """ + To improve referencing this code from Kotlin, consider adding \ explicit nullness information here with either `@NonNull` or `@Nullable`. """, - category = Category.INTEROPERABILITY_KOTLIN, - priority = 1, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - - @JvmField - val KOTLIN_PROPERTY = Issue.create( - id = "FirebaseKotlinPropertyAccess", - briefDescription = "Kotlin Property Access", - - explanation = """ + category = Category.INTEROPERABILITY_KOTLIN, + priority = 1, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + + @JvmField + val KOTLIN_PROPERTY = + Issue.create( + id = "FirebaseKotlinPropertyAccess", + briefDescription = "Kotlin Property Access", + explanation = + """ For a method to be represented as a property in Kotlin, strict “bean”-style prefixing must be used. Accessor methods require a ‘get’ prefix or for boolean-returning methods an ‘is’ prefix can be used. """, - category = Category.INTEROPERABILITY_KOTLIN, - priority = 1, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - - private fun isKotlinHardKeyword(keyword: String): Boolean { - // From https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java - when (keyword) { - "as", - "break", - "class", - "continue", - "do", - "else", - "false", - "for", - "fun", - "if", - "in", - "interface", - "is", - "null", - "object", - "package", - "return", - "super", - "this", - "throw", - "true", - "try", - "typealias", - "typeof", - "val", - "var", - "when", - "while" - -> return true - } - - return false - } + category = Category.INTEROPERABILITY_KOTLIN, + priority = 1, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + + private fun isKotlinHardKeyword(keyword: String): Boolean { + // From + // https://github.com/JetBrains/kotlin/blob/master/core/descriptors/src/org/jetbrains/kotlin/renderer/KeywordStringsGenerated.java + when (keyword) { + "as", + "break", + "class", + "continue", + "do", + "else", + "false", + "for", + "fun", + "if", + "in", + "interface", + "is", + "null", + "object", + "package", + "return", + "super", + "this", + "throw", + "true", + "try", + "typealias", + "typeof", + "val", + "var", + "when", + "while" -> return true + } + + return false } - - override fun getApplicableUastTypes(): List>? { - return listOf(UMethod::class.java, UField::class.java) + } + + override fun getApplicableUastTypes(): List>? { + return listOf(UMethod::class.java, UField::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler? { + // using deprecated psi field here instead of sourcePsi because the IDE + // still uses older version of UAST + if (isKotlin(context.uastFile?.sourcePsi)) { + // These checks apply only to Java code + return null } + return JavaVisitor(context) + } - override fun createUastHandler(context: JavaContext): UElementHandler? { - // using deprecated psi field here instead of sourcePsi because the IDE - // still uses older version of UAST - if (isKotlin(context.uastFile?.sourcePsi)) { - // These checks apply only to Java code - return null - } - return JavaVisitor(context) - } + class JavaVisitor(private val context: JavaContext) : UElementHandler() { + private val checkForKeywords = true + private val checkNullness = true + private val checkLambdaLast = true + private val checkPropertyAccess = true - class JavaVisitor(private val context: JavaContext) : UElementHandler() { - private val checkForKeywords = true - private val checkNullness = true - private val checkLambdaLast = true - private val checkPropertyAccess = true - - override fun visitMethod(node: UMethod) { - if (isPublicApi(node)) { - val methodName = node.name - - if (checkForKeywords) { - ensureNonKeyword(methodName, node, "method") - } - - if (checkPropertyAccess && isLikelySetter(methodName, node)) { - ensureValidProperty(node, methodName) - } - - if (checkLambdaLast) { - ensureLambdaLastParameter(node) - } - - if (checkNullness) { - val type = node.returnType - if (type != null) { // not a constructor - ensureNullnessKnown(node, type) - } - for (parameter in node.uastParameters) { - ensureNullnessKnown(parameter, parameter.type) - } - } - } + override fun visitMethod(node: UMethod) { + if (isPublicApi(node)) { + val methodName = node.name + + if (checkForKeywords) { + ensureNonKeyword(methodName, node, "method") } - override fun visitField(node: UField) { - if (isPublicApi(node)) { - if (checkForKeywords) { - ensureNonKeyword(node.name, node, "field") - } - if (checkNullness) { - ensureNullnessKnown(node, node.type) - } - } + if (checkPropertyAccess && isLikelySetter(methodName, node)) { + ensureValidProperty(node, methodName) } - private fun isLikelySetter( - methodName: String, - node: UMethod - ): Boolean { - return methodName.startsWith("set") && methodName.length > 3 && - Character.isUpperCase(methodName[3]) && - node.uastParameters.size == 1 && - context.evaluator.isPublic(node) && - !context.evaluator.isStatic(node) + if (checkLambdaLast) { + ensureLambdaLastParameter(node) } - private fun isPublicApi(node: UDeclaration): Boolean { - if (!isJavaPublic(node)) { - return false - } - if (node is PsiJavaDocumentedElement) { - node.docComment?.findTagByName("hide")?.let { - return false - } - } + if (checkNullness) { + val type = node.returnType + if (type != null) { // not a constructor + ensureNullnessKnown(node, type) + } + for (parameter in node.uastParameters) { + ensureNullnessKnown(parameter, parameter.type) + } + } + } + } - if (node is PsiMember) { - var curNode = node.containingClass - while (curNode != null) { - curNode.docComment?.findTagByName("hide")?.let { - return false - } - curNode = curNode.containingClass - } - } + override fun visitField(node: UField) { + if (isPublicApi(node)) { + if (checkForKeywords) { + ensureNonKeyword(node.name, node, "field") + } + if (checkNullness) { + ensureNullnessKnown(node, node.type) + } + } + } - val psiPackage: PsiPackage = context.evaluator.getPackage(node as PsiElement)!! - psiPackage.getFiles(GlobalSearchScope.projectScope(psiPackage.project)).find { - it.name == "package-info.java" - }?.let { - if (it.viewProvider.contents.toString().matches(Regex(".*/\\*\\*.*@hide.*\\*/.*\n\\s*package.*", RegexOption.DOT_MATCHES_ALL))) { - return false - } - } + private fun isLikelySetter(methodName: String, node: UMethod): Boolean { + return methodName.startsWith("set") && + methodName.length > 3 && + Character.isUpperCase(methodName[3]) && + node.uastParameters.size == 1 && + context.evaluator.isPublic(node) && + !context.evaluator.isStatic(node) + } - return true + private fun isPublicApi(node: UDeclaration): Boolean { + if (!isJavaPublic(node)) { + return false + } + if (node is PsiJavaDocumentedElement) { + node.docComment?.findTagByName("hide")?.let { + return false } + } - private fun isJavaPublic(node: UDeclaration): Boolean { - val evaluator = context.evaluator - if (evaluator.isPublic(node) || evaluator.isProtected(node)) { - val cls = node.getParentOfType(UClass::class.java) ?: return true - return evaluator.isPublic(cls) && cls !is UAnonymousClass - } - + if (node is PsiMember) { + var curNode = node.containingClass + while (curNode != null) { + curNode.docComment?.findTagByName("hide")?.let { return false + } + curNode = curNode.containingClass + } + } + + val psiPackage: PsiPackage = context.evaluator.getPackage(node as PsiElement)!! + psiPackage + .getFiles(GlobalSearchScope.projectScope(psiPackage.project)) + .find { it.name == "package-info.java" } + ?.let { + if ( + it.viewProvider.contents + .toString() + .matches( + Regex(".*/\\*\\*.*@hide.*\\*/.*\n\\s*package.*", RegexOption.DOT_MATCHES_ALL) + ) + ) { + return false + } } - private fun ensureValidProperty(setter: UMethod, methodName: String) { - val cls = setter.getContainingUClass() ?: return - val propertySuffix = methodName.substring(3) - val propertyName = propertySuffix.decapitalize() - val getterName1 = "get$propertySuffix" - val getterName2 = "is$propertySuffix" - val badGetterName = "has$propertySuffix" - var getter: PsiMethod? = null - var badGetter: UMethod? = null - cls.methods.forEach { - if (it.parameters.isEmpty()) { - val name = it.name - if (name == getterName1 || name == getterName2) { - getter = it - } else if ((name == badGetterName || name == propertyName || - name.endsWith(propertySuffix)) && - context.evaluator.isPublic(it) && - !it.isConstructor && - it.returnType == setter.uastParameters.firstOrNull()?.type - ) { - badGetter = it - } - } - } + return true + } - if (getter == null) { - // Look for inherited methods - cls.superClass?.let { superClass -> - for (inherited in superClass.findMethodsByName(getterName1, true)) { - if (inherited.parameterList.parametersCount == 0) { - getter = inherited - break - } - } - if (getter == null) { - for (inherited in superClass.findMethodsByName(getterName2, true)) { - if (inherited.parameterList.parametersCount == 0) { - getter = inherited - break - } - } - } - } - } + private fun isJavaPublic(node: UDeclaration): Boolean { + val evaluator = context.evaluator + if (evaluator.isPublic(node) || evaluator.isProtected(node)) { + val cls = node.getParentOfType(UClass::class.java) ?: return true + return evaluator.isPublic(cls) && cls !is UAnonymousClass + } - if (getter != null && getter !is PsiCompiledElement) { - @Suppress("NAME_SHADOWING") // compiler gets confused about getter nullness - val getter: PsiMethod = getter!! - - // enforce public and not static - if (!context.evaluator.isPublic(getter)) { - val message = "This getter should be public such that `$propertyName` can " + - "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" - val location = context.getNameLocation(getter) - context.report(KOTLIN_PROPERTY, getter, location, message) - return - } - - if (context.evaluator.isStatic(getter)) { - var staticElement: PsiElement? = null - val modifierList = getter.modifierList - // Try to find the static modifier itself - if (modifierList.hasExplicitModifier(PsiModifier.STATIC)) { - var child: PsiElement? = modifierList.firstChild - while (child != null) { - if (child is PsiKeyword && PsiKeyword.STATIC == child.text) { - staticElement = child - break - } - child = child.nextSibling - } - } - val location = if (staticElement != null) { - context.getLocation(staticElement) - } else { - context.getNameLocation(getter) - } - val message = - "This getter should not be static such that `$propertyName` can " + - "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" - context.report( - KOTLIN_PROPERTY, - location.source as? PsiElement ?: setter, location, message - ) - return - } - - val setterParameterType = setter.uastParameters.first().type - if (setterParameterType != getter.returnType && - !hasSetter(cls, getter.returnType, setter.name) && - !isTypeVariableReference(setterParameterType) - ) { - val message = - "The getter return type (`${getter.returnType?.presentableText}`) and setter parameter type (`${setterParameterType.presentableText}`) getter and setter methods for property `$propertyName` should have exactly the same type to allow " + - "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" - val location = getPropertyLocation(getter, setter) - context.report( - KOTLIN_PROPERTY, - location.source as? PsiElement ?: setter, location, message - ) - return - } - - // Make sure that if the getter is inherited, it has the same return type - for (superMethod in getter.findSuperMethods()) { - if (superMethod.containingClass?.isInterface != true) { - val superReturnType = superMethod.returnType ?: return - val getterType = getter.returnType - if (superReturnType != getterType) { - val message = - "The getter return type (`${getterType?.presentableText}`)" + - " is not the same as the setter return type " + - "(`${superReturnType.presentableText}`); they should have " + - "exactly the same type to allow " + - "`${propertySuffix.decapitalize()}` " + - "be accessed as a property from Kotlin; see " + - "https://android.github.io/kotlin-guides/interop.html#property-prefixes" - val location = getPropertyLocation(getter, setter) - context.report( - KOTLIN_PROPERTY, - location.source as? PsiElement ?: setter, location, message - ) - return - } - } - } - } else if (badGetter != null && - // Don't complain about overrides; we can't rename those - !badGetter!!.findSuperMethods().any() && - // Don't complain if the matched bad getter method already has its own - // match - run { - val matchingName = - "set${badGetter!!.name.removePrefix("is").removePrefix("get").removePrefix("has")}" - - methodName == matchingName || cls.methods.none { it.name == matchingName } - } - ) { - val name1 = badGetter!!.name - if (name1.startsWith("is") && methodName.startsWith("setIs") && - name1[2].isUpperCase() - ) { - val newProperty = name1[2].toLowerCase() + name1.substring(3) - val message = - "This method should be called `set${newProperty.capitalize()}` such " + - "that (along with the `$name1` getter) Kotlin code can access it " + - "as a property (`$newProperty`); see " + - "https://android.github.io/kotlin-guides/interop.html#property-prefixes" - val location = context.getNameLocation(setter) - context.report(KOTLIN_PROPERTY, setter, location, message) - return - } - - val location = context.getNameLocation(badGetter!!) - val message = - "This method should be called `get$propertySuffix` such that `$propertyName` can " + - "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" - context.report(KOTLIN_PROPERTY, badGetter, location, message) + return false + } + + private fun ensureValidProperty(setter: UMethod, methodName: String) { + val cls = setter.getContainingUClass() ?: return + val propertySuffix = methodName.substring(3) + val propertyName = propertySuffix.decapitalize() + val getterName1 = "get$propertySuffix" + val getterName2 = "is$propertySuffix" + val badGetterName = "has$propertySuffix" + var getter: PsiMethod? = null + var badGetter: UMethod? = null + cls.methods.forEach { + if (it.parameters.isEmpty()) { + val name = it.name + if (name == getterName1 || name == getterName2) { + getter = it + } else if ( + (name == badGetterName || name == propertyName || name.endsWith(propertySuffix)) && + context.evaluator.isPublic(it) && + !it.isConstructor && + it.returnType == setter.uastParameters.firstOrNull()?.type + ) { + badGetter = it + } + } + } + + if (getter == null) { + // Look for inherited methods + cls.javaPsi.superClass?.let { superClass -> + for (inherited in superClass.findMethodsByName(getterName1, true)) { + if (inherited.parameterList.parametersCount == 0) { + getter = inherited + break + } + } + if (getter == null) { + for (inherited in superClass.findMethodsByName(getterName2, true)) { + if (inherited.parameterList.parametersCount == 0) { + getter = inherited + break + } } + } + } + } + + if (getter != null && getter !is PsiCompiledElement) { + @Suppress("NAME_SHADOWING") // compiler gets confused about getter nullness + val getter: PsiMethod = getter!! + + // enforce public and not static + if (!context.evaluator.isPublic(getter)) { + val message = + "This getter should be public such that `$propertyName` can " + + "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" + val location = context.getNameLocation(getter) + context.report(KOTLIN_PROPERTY, getter, location, message) + return } - private fun isTypeVariableReference(type: PsiType): Boolean { - if (type is PsiClassType) { - val cls = type.resolve() ?: return false - return cls is PsiTypeParameter + if (context.evaluator.isStatic(getter)) { + var staticElement: PsiElement? = null + val modifierList = getter.modifierList + // Try to find the static modifier itself + if (modifierList.hasExplicitModifier(PsiModifier.STATIC)) { + var child: PsiElement? = modifierList.firstChild + while (child != null) { + if (child is PsiKeyword && PsiKeyword.STATIC == child.text) { + staticElement = child + break + } + child = child.nextSibling + } + } + val location = + if (staticElement != null) { + context.getLocation(staticElement) } else { - return false + context.getNameLocation(getter) } + val message = + "This getter should not be static such that `$propertyName` can " + + "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" + context.report( + KOTLIN_PROPERTY, + location.source as? PsiElement ?: setter, + location, + message + ) + return } - /** Returns true if the given class has a (possibly inherited) setter of the given type */ - private fun hasSetter(cls: UClass, type: PsiType?, setterName: String): Boolean { - for (method in cls.findMethodsByName(setterName, true)) { - val parameterList = method.parameterList - val parameters = parameterList.parameters - if (parameters.size == 1 && parameters[0].type == type) { - return true - } - } - - return false + val setterParameterType = setter.uastParameters.first().type + if ( + setterParameterType != getter.returnType && + !hasSetter(cls, getter.returnType, setter.name) && + !isTypeVariableReference(setterParameterType) + ) { + val message = + "The getter return type (`${getter.returnType?.presentableText}`) and setter parameter type (`${setterParameterType.presentableText}`) getter and setter methods for property `$propertyName` should have exactly the same type to allow " + + "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" + val location = getPropertyLocation(getter, setter) + context.report( + KOTLIN_PROPERTY, + location.source as? PsiElement ?: setter, + location, + message + ) + return } - private fun getPropertyLocation( - location1: PsiMethod, - location2: PsiMethod - ): Location { - val primary: PsiMethod - val secondary: PsiMethod - - if (location1 is PsiCompiledElement) { - primary = location2 - secondary = location1 - } else { - primary = location1 - secondary = location2 + // Make sure that if the getter is inherited, it has the same return type + for (superMethod in getter.findSuperMethods()) { + if (superMethod.containingClass?.isInterface != true) { + val superReturnType = superMethod.returnType ?: return + val getterType = getter.returnType + if (superReturnType != getterType) { + val message = + "The getter return type (`${getterType?.presentableText}`)" + + " is not the same as the setter return type " + + "(`${superReturnType.presentableText}`); they should have " + + "exactly the same type to allow " + + "`${propertySuffix.decapitalize()}` " + + "be accessed as a property from Kotlin; see " + + "https://android.github.io/kotlin-guides/interop.html#property-prefixes" + val location = getPropertyLocation(getter, setter) + context.report( + KOTLIN_PROPERTY, + location.source as? PsiElement ?: setter, + location, + message + ) + return } - - return context.getNameLocation(primary).withSecondary( - context.getNameLocation(secondary), - "${if (secondary.name.startsWith("set")) "Setter" else "Getter"} here" - ) + } + } + } else if ( + badGetter != null && + // Don't complain about overrides; we can't rename those + !badGetter!!.findSuperMethods().any() && + // Don't complain if the matched bad getter method already has its own + // match + run { + val matchingName = + "set${badGetter!!.name.removePrefix("is").removePrefix("get").removePrefix("has")}" + + methodName == matchingName || cls.methods.none { it.name == matchingName } + } + ) { + val name1 = badGetter!!.name + if (name1.startsWith("is") && methodName.startsWith("setIs") && name1[2].isUpperCase()) { + val newProperty = name1[2].lowercaseChar() + name1.substring(3) + val message = + "This method should be called `set${newProperty.capitalize()}` such " + + "that (along with the `$name1` getter) Kotlin code can access it " + + "as a property (`$newProperty`); see " + + "https://android.github.io/kotlin-guides/interop.html#property-prefixes" + val location = context.getNameLocation(setter) + context.report(KOTLIN_PROPERTY, setter, location, message) + return } - private fun ensureNullnessKnown( - node: UDeclaration, - type: PsiType - ) { - if (type is PsiPrimitiveType) { - return - } - if (node is UField && - node.modifierList?.hasModifierProperty(PsiModifier.FINAL) == true) { - return - } - for (annotation in node.uAnnotations) { - val name = annotation.qualifiedName ?: continue - - if (isNullableAnnotation(name)) { - if (isToStringMethod(node)) { - val location = context.getLocation(annotation) - val message = "Unexpected `@Nullable`: `toString` should never return null" - context.report(PLATFORM_NULLNESS, node as UElement, location, message) - } - return - } - - if (isNonNullAnnotation(name)) { - if (isEqualsParameter(node)) { - val location = context.getLocation(annotation) - val message = - "Unexpected @NonNull: The `equals` contract allows the parameter to be null" - context.report(PLATFORM_NULLNESS, node as UElement, location, message) - } - return - } - } + val location = context.getNameLocation(badGetter!!) + val message = + "This method should be called `get$propertySuffix` such that `$propertyName` can " + + "be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes" + context.report(KOTLIN_PROPERTY, badGetter, location, message) + } + } - // Known nullability: don't complain - if (isEqualsParameter(node) || isToStringMethod(node) || isVarargParameter(node)) { - return - } + private fun isTypeVariableReference(type: PsiType): Boolean { + if (type is PsiClassType) { + val cls = type.resolve() ?: return false + return cls is PsiTypeParameter + } else { + return false + } + } - // Annotation members cannot be null - if (node is UMethod) { - node.getContainingUClass()?.let { - if (it.isAnnotationType) { - return - } - } - } + private fun String.capitalize() = replaceFirstChar { it.uppercase() } - // Skip deprecated members? - if (IGNORE_DEPRECATED) { - val deprecatedNode = - if (node is UParameter) { - node.uastParent - } else { - node - } - if ((deprecatedNode?.sourcePsi as? PsiDocCommentOwner)?.isDeprecated == true) { - return - } - var curr = deprecatedNode - while (curr != null) { - curr = curr.getContainingUClass() ?: break - if ((curr.sourcePsi as? PsiDocCommentOwner)?.isDeprecated == true) { - return - } - } - } + private fun String.decapitalize() = replaceFirstChar { it.lowercase() } - val location: Location = - when (node) { - is UVariable -> // UParameter, UField - context.getLocation(node.typeReference ?: return) - is UMethod -> context.getLocation(node.returnTypeElement ?: return) - else -> return - } - val replaceLocation = if (node is UParameter) { - location - } else if (node is UMethod && node.modifierList != null) { - // Place the insertion point at the modifiers such that we don't - // insert the annotation for example after the "public" keyword. - // We also don't want to place it on the method range itself since - // that would place it before the method comments. - context.getLocation(node.modifierList) - } else if (node is UField && node.modifierList != null) { - // Ditto for fields - context.getLocation(node.modifierList!!) - } else { - return - } - val message = "Unknown nullability; explicitly declare as `@Nullable` or `@NonNull`" + - " to improve Kotlin interoperability; see " + - "https://android.github.io/kotlin-guides/interop.html#nullability-annotations" - val fix = LintFix.create().alternatives( - LintFix.create() - .replace() - .name("Annotate @NonNull") - .range(replaceLocation) - .beginning() - .shortenNames() - .reformat(true) - .with("$nonNullAnnotation ") - .build(), - LintFix.create() - .replace() - .name("Annotate @Nullable") - .range(replaceLocation) - .beginning() - .shortenNames() - .reformat(true) - .with("$nullableAnnotation ") - .build() - ) - context.report(PLATFORM_NULLNESS, node as UElement, location, message, fix) + /** Returns true if the given class has a (possibly inherited) setter of the given type */ + private fun hasSetter(cls: UClass, type: PsiType?, setterName: String): Boolean { + for (method in cls.findMethodsByName(setterName, true)) { + val parameterList = method.parameterList + val parameters = parameterList.parameters + if (parameters.size == 1 && parameters[0].type == type) { + return true } + } - private fun isEqualsParameter(node: UDeclaration): Boolean { - if (node is UParameter) { - val method = node.getContainingUMethod() ?: return false - if (method.name == "equals" && method.uastParameters.size == 1) { - return true - } - } + return false + } - return false - } + private fun getPropertyLocation(location1: PsiMethod, location2: PsiMethod): Location { + val primary: PsiMethod + val secondary: PsiMethod + + if (location1 is PsiCompiledElement) { + primary = location2 + secondary = location1 + } else { + primary = location1 + secondary = location2 + } + + return context + .getNameLocation(primary) + .withSecondary( + context.getNameLocation(secondary), + "${if (secondary.name.startsWith("set")) "Setter" else "Getter"} here" + ) + } - private fun isVarargParameter(node: UDeclaration): Boolean { - return node is UParameter && node.isVarArgs + private fun ensureNullnessKnown(node: UDeclaration, type: PsiType) { + if (type is PsiPrimitiveType) { + return + } + if (node is UField && node.modifierList?.hasModifierProperty(PsiModifier.FINAL) == true) { + return + } + for (annotation in node.uAnnotations) { + val name = annotation.qualifiedName ?: continue + + if (isNullableAnnotation(name)) { + if (isToStringMethod(node)) { + val location = context.getLocation(annotation) + val message = "Unexpected `@Nullable`: `toString` should never return null" + context.report(PLATFORM_NULLNESS, node as UElement, location, message) + } + return } - private fun isToStringMethod(node: UDeclaration): Boolean { - if (node is UMethod) { - val method = node - if (method.name == "toString" && method.uastParameters.isEmpty()) { - return true - } - } + if (isNonNullAnnotation(name)) { + if (isEqualsParameter(node)) { + val location = context.getLocation(annotation) + val message = + "Unexpected @NonNull: The `equals` contract allows the parameter to be null" + context.report(PLATFORM_NULLNESS, node as UElement, location, message) + } + return + } + } + + // Known nullability: don't complain + if (isEqualsParameter(node) || isToStringMethod(node) || isVarargParameter(node)) { + return + } + + // Annotation members cannot be null + if (node is UMethod) { + node.getContainingUClass()?.let { + if (it.isAnnotationType) { + return + } + } + } + + // Skip deprecated members? + if (IGNORE_DEPRECATED) { + val deprecatedNode = + if (node is UParameter) { + node.uastParent + } else { + node + } + if ((deprecatedNode?.sourcePsi as? PsiDocCommentOwner)?.isDeprecated == true) { + return + } + var curr = deprecatedNode + while (curr != null) { + curr = curr.getContainingUClass() ?: break + if ((curr.sourcePsi as? PsiDocCommentOwner)?.isDeprecated == true) { + return + } + } + } + + val location: Location = + when (node) { + is UVariable -> // UParameter, UField + context.getLocation(node.typeReference ?: return) + is UMethod -> context.getLocation(node.returnTypeElement ?: return) + else -> return + } + val replaceLocation = + if (node is UParameter) { + location + } else if (node is UMethod) { + // Place the insertion point at the modifiers such that we don't + // insert the annotation for example after the "public" keyword. + // We also don't want to place it on the method range itself since + // that would place it before the method comments. + context.getLocation(node.modifierList) + } else if (node is UField && node.modifierList != null) { + // Ditto for fields + context.getLocation(node.modifierList!!) + } else { + return + } + val message = + "Unknown nullability; explicitly declare as `@Nullable` or `@NonNull`" + + " to improve Kotlin interoperability; see " + + "https://android.github.io/kotlin-guides/interop.html#nullability-annotations" + val fix = + LintFix.create() + .alternatives( + LintFix.create() + .replace() + .name("Annotate @NonNull") + .range(replaceLocation) + .beginning() + .shortenNames() + .reformat(true) + .with("$nonNullAnnotation ") + .build(), + LintFix.create() + .replace() + .name("Annotate @Nullable") + .range(replaceLocation) + .beginning() + .shortenNames() + .reformat(true) + .with("$nullableAnnotation ") + .build() + ) + context.report(PLATFORM_NULLNESS, node as UElement, location, message, fix) + } - return false + private fun isEqualsParameter(node: UDeclaration): Boolean { + if (node is UParameter) { + val method = node.getContainingUMethod() ?: return false + if (method.name == "equals" && method.uastParameters.size == 1) { + return true } + } - private var nonNullAnnotation: String = "@androidx.annotation.NonNull" - private var nullableAnnotation: String? = "@androidx.annotation.Nullable" + return false + } - private fun isNullableAnnotation(qualifiedName: String): Boolean { - return qualifiedName.endsWith("Nullable") - } + private fun isVarargParameter(node: UDeclaration): Boolean { + return node is UParameter && node.isVarArgs + } - private fun isNonNullAnnotation(qualifiedName: String): Boolean { - return qualifiedName.endsWith("NonNull") || - qualifiedName.endsWith("NotNull") || - qualifiedName.endsWith("Nonnull") + private fun isToStringMethod(node: UDeclaration): Boolean { + if (node is UMethod) { + val method = node + if (method.name == "toString" && method.uastParameters.isEmpty()) { + return true } + } - private fun ensureNonKeyword(name: String, node: UDeclaration, typeLabel: String) { - if (isKotlinHardKeyword(name)) { - // See if this method is overriding some other method; in that case - // we don't have a choice here. - if (node is UMethod && context.evaluator.isOverride(node)) { - return - } - val message = - "Avoid $typeLabel names that are Kotlin hard keywords (\"$name\"); see " + - "https://android.github.io/kotlin-guides/interop.html#no-hard-keywords" - context.report( - NO_HARD_KOTLIN_KEYWORDS, - node as UElement, - context.getNameLocation(node as UElement), - message - ) - } + return false + } + + private var nonNullAnnotation: String = "@androidx.annotation.NonNull" + private var nullableAnnotation: String? = "@androidx.annotation.Nullable" + + private fun isNullableAnnotation(qualifiedName: String): Boolean { + return qualifiedName.endsWith("Nullable") + } + + private fun isNonNullAnnotation(qualifiedName: String): Boolean { + return qualifiedName.endsWith("NonNull") || + qualifiedName.endsWith("NotNull") || + qualifiedName.endsWith("Nonnull") + } + + private fun ensureNonKeyword(name: String, node: UDeclaration, typeLabel: String) { + if (isKotlinHardKeyword(name)) { + // See if this method is overriding some other method; in that case + // we don't have a choice here. + if (node is UMethod && context.evaluator.isOverride(node)) { + return } + val message = + "Avoid $typeLabel names that are Kotlin hard keywords (\"$name\"); see " + + "https://android.github.io/kotlin-guides/interop.html#no-hard-keywords" + context.report( + NO_HARD_KOTLIN_KEYWORDS, + node as UElement, + context.getNameLocation(node as UElement), + message + ) + } + } - private fun ensureLambdaLastParameter(method: UMethod) { - val parameters = method.uastParameters - if (parameters.size > 1) { - // Make sure that SAM-compatible parameters are last - val lastIndex = parameters.size - 1 - if (!isFunctionalInterface(parameters[lastIndex].type)) { - for (i in lastIndex - 1 downTo 0) { - val parameter = parameters[i] - if (isFunctionalInterface(parameter.type)) { - val message = - "Functional interface parameters (such as parameter ${i + 1}, \"${parameter.name}\", in ${ + private fun ensureLambdaLastParameter(method: UMethod) { + val parameters = method.uastParameters + if (parameters.size > 1) { + // Make sure that SAM-compatible parameters are last + val lastIndex = parameters.size - 1 + if (!isFunctionalInterface(parameters[lastIndex].type)) { + for (i in lastIndex - 1 downTo 0) { + val parameter = parameters[i] + if (isFunctionalInterface(parameter.type)) { + val message = + "Functional interface parameters (such as parameter ${i + 1}, \"${parameter.name}\", in ${ method.containingClass?.qualifiedName}.${method.name }) should be last to improve Kotlin interoperability; see " + - "https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions" - context.report( - LAMBDA_LAST, - method, - context.getLocation(parameters[lastIndex] as UElement), - message - ) - break - } - } - } + "https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions" + context.report( + LAMBDA_LAST, + method, + context.getLocation(parameters[lastIndex] as UElement), + message + ) + break } + } } + } + } - private fun isFunctionalInterface(type: PsiType): Boolean { - if (type !is PsiClassType) { - return false - } + private fun isFunctionalInterface(type: PsiType): Boolean { + if (type !is PsiClassType) { + return false + } - val cls = type.resolve() ?: return false - if (!cls.isInterface) { - return false - } + val cls = type.resolve() ?: return false + if (!cls.isInterface) { + return false + } - var abstractCount = 0 - for (method in cls.methods) { - if (method.modifierList.hasModifierProperty(PsiModifier.ABSTRACT)) { - abstractCount++ - } + var abstractCount = 0 + for (method in cls.methods) { + if (method.modifierList.hasModifierProperty(PsiModifier.ABSTRACT)) { + abstractCount++ + } + } + + if (abstractCount != 1) { + // Try a little harder; we don't want to count methods that are overrides + if (abstractCount > 1) { + abstractCount = 0 + for (method in cls.methods) { + if ( + method.modifierList.hasModifierProperty(PsiModifier.ABSTRACT) && + !context.evaluator.isOverride(method, true) + ) { + abstractCount++ } + } + } - if (abstractCount != 1) { - // Try a little harder; we don't want to count methods that are overrides - if (abstractCount > 1) { - abstractCount = 0 - for (method in cls.methods) { - if (method.modifierList.hasModifierProperty(PsiModifier.ABSTRACT) && - !context.evaluator.isOverride(method, true) - ) { - abstractCount++ - } - } - } - - if (abstractCount != 1) { - return false - } - } + if (abstractCount != 1) { + return false + } + } - if (cls.superClass?.isInterface == true) { - return false - } + if (cls.superClass?.isInterface == true) { + return false + } - return true - } + return true } + } } diff --git a/tools/lint/src/main/kotlin/ManifestElementHasNoExportedAttributeDetector.kt b/tools/lint/src/main/kotlin/ManifestElementHasNoExportedAttributeDetector.kt index ac0a43e2b73..71b5b1f5c4d 100644 --- a/tools/lint/src/main/kotlin/ManifestElementHasNoExportedAttributeDetector.kt +++ b/tools/lint/src/main/kotlin/ManifestElementHasNoExportedAttributeDetector.kt @@ -25,39 +25,48 @@ import com.android.tools.lint.detector.api.XmlContext import com.android.tools.lint.detector.api.XmlScanner import org.w3c.dom.Element +@Suppress("DetectorIsMissingAnnotations") class ManifestElementHasNoExportedAttributeDetector : Detector(), XmlScanner { - enum class Component(val xmlName: String) { - ACTIVITY("activity"), - ACTIVITY_ALIAS("activity-alias"), - PROVIDER("provider"), - RECEIVER("receiver"), - SERVICE("service"), - } + enum class Component(val xmlName: String) { + ACTIVITY("activity"), + ACTIVITY_ALIAS("activity-alias"), + PROVIDER("provider"), + RECEIVER("receiver"), + SERVICE("service"), + } - companion object { - internal val EXPORTED_MISSING_ISSUE: Issue = Issue.create( - "ManifestElementHasNoExportedAttribute", - "`android:exported` attribute missing on element", - "Leaving this attribute out may unintentionally lead to an exported component, please " + - "specify the value explicitly.", - Category.SECURITY, - 1, - Severity.ERROR, - Implementation(ManifestElementHasNoExportedAttributeDetector::class.java, Scope.MANIFEST_SCOPE)) - } + companion object { + internal val EXPORTED_MISSING_ISSUE: Issue = + Issue.create( + "ManifestElementHasNoExportedAttribute", + "`android:exported` attribute missing on element", + "Leaving this attribute out may unintentionally lead to an exported component, please " + + "specify the value explicitly.", + Category.SECURITY, + 1, + Severity.ERROR, + Implementation( + ManifestElementHasNoExportedAttributeDetector::class.java, + Scope.MANIFEST_SCOPE + ) + ) + } - override fun getApplicableElements(): Collection? = Component.values().map { it.xmlName } + override fun getApplicableElements(): Collection? = Component.values().map { it.xmlName } - override fun visitElement(context: XmlContext, element: Element) { - val value = element.getAttributeNS(SdkConstants.ANDROID_URI, "exported") - value.takeIf { it.isEmpty() }?.let { - context.report( - EXPORTED_MISSING_ISSUE, - element, - context.getLocation(element), - "Set `android:exported` attribute explicitly. As the implicit default value " + - "is insecure.") - } - } + override fun visitElement(context: XmlContext, element: Element) { + val value = element.getAttributeNS(SdkConstants.ANDROID_URI, "exported") + value + .takeIf { it.isEmpty() } + ?.let { + context.report( + EXPORTED_MISSING_ISSUE, + element, + context.getLocation(element), + "Set `android:exported` attribute explicitly. As the implicit default value " + + "is insecure." + ) + } + } } diff --git a/tools/lint/src/main/kotlin/NonAndroidxNullabilityDetector.kt b/tools/lint/src/main/kotlin/NonAndroidxNullabilityDetector.kt index f80896f98cb..2d351c7d05c 100644 --- a/tools/lint/src/main/kotlin/NonAndroidxNullabilityDetector.kt +++ b/tools/lint/src/main/kotlin/NonAndroidxNullabilityDetector.kt @@ -35,96 +35,102 @@ import org.jetbrains.uast.UMethod private val NULLABLE_ANNOTATIONS = listOf("Nullable", "CheckForNull") private val NOT_NULL_ANNOTATIONS = listOf("NonNull", "NotNull", "Nonnull") -private val ANDROIDX_ANNOTATIONS = listOf( - "androidx.annotation.Nullable", - "androidx.annotation.NonNull", - "android.support.annotation.Nullable", - "android.support.annotation.NonNull") - +private val ANDROIDX_ANNOTATIONS = + listOf( + "androidx.annotation.Nullable", + "androidx.annotation.NonNull", + "android.support.annotation.Nullable", + "android.support.annotation.NonNull" + ) + +@Suppress("DetectorIsMissingAnnotations") class NonAndroidxNullabilityDetector : Detector(), SourceCodeScanner { - companion object Issues { - private val IMPLEMENTATION = Implementation( - NonAndroidxNullabilityDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - - @JvmField - val NON_ANDROIDX_NULLABILITY = Issue.create( - id = "FirebaseNonAndroidxNullability", - briefDescription = "Use androidx nullability annotations.", - - explanation = "Use androidx nullability annotations instead.", - category = Category.COMPLIANCE, - priority = 1, - severity = Severity.ERROR, - enabledByDefault = false, - implementation = IMPLEMENTATION - ) + companion object Issues { + private val IMPLEMENTATION = + Implementation(NonAndroidxNullabilityDetector::class.java, Scope.JAVA_FILE_SCOPE) + + @JvmField + val NON_ANDROIDX_NULLABILITY = + Issue.create( + id = "FirebaseNonAndroidxNullability", + briefDescription = "Use androidx nullability annotations", + explanation = "Use androidx nullability annotations instead.", + category = Category.COMPLIANCE, + priority = 1, + severity = Severity.ERROR, + enabledByDefault = false, + implementation = IMPLEMENTATION + ) + } + + override fun getApplicableUastTypes(): List>? { + return listOf(UClass::class.java, UMethod::class.java, UField::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler? { + // using deprecated psi field here instead of sourcePsi because the IDE + // still uses older version of UAST + if (isKotlin(context.uastFile?.sourcePsi)) { + // These checks apply only to Java code + return null + } + return Visitor(context) + } + + class Visitor(private val context: JavaContext) : UElementHandler() { + override fun visitClass(node: UClass) { + doVisit(node) } - override fun getApplicableUastTypes(): List>? { - return listOf(UClass::class.java, UMethod::class.java, UField::class.java) + override fun visitMethod(node: UMethod) { + doVisit(node) + for (parameter in node.uastParameters) { + doVisit(parameter) + } } - override fun createUastHandler(context: JavaContext): UElementHandler? { - // using deprecated psi field here instead of sourcePsi because the IDE - // still uses older version of UAST - if (isKotlin(context.uastFile?.sourcePsi)) { - // These checks apply only to Java code - return null - } - return Visitor(context) + override fun visitField(node: UField) { + doVisit(node) } - class Visitor(private val context: JavaContext) : UElementHandler() { - override fun visitClass(node: UClass) { - doVisit(node) - } - - override fun visitMethod(node: UMethod) { - doVisit(node) - for (parameter in node.uastParameters) { - doVisit(parameter) - } - } - - override fun visitField(node: UField) { - doVisit(node) - } - - private fun doVisit(node: UDeclaration) { - for (annotation in node.uAnnotations) { - ensureAndroidNullability(context, annotation) - } - } - - private fun ensureAndroidNullability(context: JavaContext, annotation: UAnnotation) { - val name = annotation.qualifiedName ?: return - - val simpleName = name.split('.').last() - - if (!isNullabilityAnnotation(simpleName) || name in ANDROIDX_ANNOTATIONS) { - return - } - - val replacement = if (simpleName in NOT_NULL_ANNOTATIONS) "NonNull" else "Nullable" - val replacementAnnotation = "@androidx.annotation.$replacement" - - val fix = LintFix.create() - .replace() - .name("Replace with $replacementAnnotation") - .range(context.getLocation(annotation)) - .all() - .shortenNames() - .reformat(true) - .with(replacementAnnotation) - .build() - - context.report(NON_ANDROIDX_NULLABILITY, context.getLocation(annotation), - "Use androidx nullability annotations.", fix) - } - - private fun isNullabilityAnnotation(name: String): Boolean = - name in NULLABLE_ANNOTATIONS || name in NOT_NULL_ANNOTATIONS + private fun doVisit(node: UDeclaration) { + for (annotation in node.uAnnotations) { + ensureAndroidNullability(context, annotation) + } } + + private fun ensureAndroidNullability(context: JavaContext, annotation: UAnnotation) { + val name = annotation.qualifiedName ?: return + + val simpleName = name.split('.').last() + + if (!isNullabilityAnnotation(simpleName) || name in ANDROIDX_ANNOTATIONS) { + return + } + + val replacement = if (simpleName in NOT_NULL_ANNOTATIONS) "NonNull" else "Nullable" + val replacementAnnotation = "@androidx.annotation.$replacement" + + val fix = + LintFix.create() + .replace() + .name("Replace with $replacementAnnotation") + .range(context.getLocation(annotation)) + .all() + .shortenNames() + .reformat(true) + .with(replacementAnnotation) + .build() + + context.report( + NON_ANDROIDX_NULLABILITY, + context.getLocation(annotation), + "Use androidx nullability annotations", + fix + ) + } + + private fun isNullabilityAnnotation(name: String): Boolean = + name in NULLABLE_ANNOTATIONS || name in NOT_NULL_ANNOTATIONS + } } diff --git a/tools/lint/src/main/kotlin/ProviderAssignmentDetector.kt b/tools/lint/src/main/kotlin/ProviderAssignmentDetector.kt index c3ad7426475..2840bd78933 100644 --- a/tools/lint/src/main/kotlin/ProviderAssignmentDetector.kt +++ b/tools/lint/src/main/kotlin/ProviderAssignmentDetector.kt @@ -25,78 +25,79 @@ import com.android.tools.lint.detector.api.SourceCodeScanner import com.intellij.psi.PsiClass import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UBinaryExpression import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UReferenceExpression import org.jetbrains.uast.getParentOfType -import org.jetbrains.uast.java.JavaUAssignmentExpression private const val PROVIDER = "com.google.firebase.inject.Provider" +@Suppress("DetectorIsMissingAnnotations") class ProviderAssignmentDetector : Detector(), SourceCodeScanner { - override fun getApplicableMethodNames() = listOf("get") + override fun getApplicableMethodNames() = listOf("get") - override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { - if (!isProviderGet(method)) { - return - } - val assignmentExpression = node - .getParentOfType( - JavaUAssignmentExpression::class.java, true) - val assignmentTarget = assignmentExpression?.leftOperand as? UReferenceExpression ?: return - - // This would only be true if assigning the result of get(), - // in cases like foo = p.get().someMethod() there would be an intermediate parent - // and we don't want to trigger in such cases. - if (assignmentExpression != node.uastParent?.uastParent) { - return - } + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isProviderGet(method)) { + return + } + val binaryOperation = node.getParentOfType(true) ?: return + if (binaryOperation.operatorIdentifier?.name != "=") return - if (hasDeferredApiAnnotation(context, assignmentExpression)) { - return - } + val assignmentTarget = binaryOperation.leftOperand as? UReferenceExpression ?: return - assignmentTarget.resolve()?.let { - if (it is PsiField) { - context.report( - INVALID_PROVIDER_ASSIGNMENT, - context.getCallLocation(node, includeReceiver = false, includeArguments = true), - "Provider.get() assignment to a field detected.") - } - } + // This would only be true if assigning the result of get(), + // in cases like foo = p.get().someMethod() there would be an intermediate parent + // and we don't want to trigger in such cases. + if (binaryOperation != node.uastParent?.uastParent) { + return } - private fun isProviderGet(method: PsiMethod): Boolean { - if (!method.parameterList.isEmpty) { - return false - } - (method.parent as? PsiClass)?.let { - return it.qualifiedName == PROVIDER - } - return false + if (hasDeferredApiAnnotation(context, binaryOperation)) { + return } - companion object { - private val IMPLEMENTATION = Implementation( - ProviderAssignmentDetector::class.java, - Scope.JAVA_FILE_SCOPE + assignmentTarget.resolve()?.let { + if (it is PsiField) { + context.report( + INVALID_PROVIDER_ASSIGNMENT, + context.getCallLocation(node, includeReceiver = false, includeArguments = true), + "`Provider.get()` assignment to a field detected" ) + } + } + } + + private fun isProviderGet(method: PsiMethod): Boolean { + if (!method.parameterList.isEmpty) { + return false + } + (method.parent as? PsiClass)?.let { + return it.qualifiedName == PROVIDER + } + return false + } - /** Calling methods on the wrong thread */ - @JvmField - val INVALID_PROVIDER_ASSIGNMENT = Issue.create( - id = "ProviderAssignment", - briefDescription = "Invalid use of Provider", + companion object { + private val IMPLEMENTATION = + Implementation(ProviderAssignmentDetector::class.java, Scope.JAVA_FILE_SCOPE) - explanation = """ - Ensures that results of Provider.get() are not stored in class fields. Doing - so may lead to bugs in the context of dynamic feature loading. Namely, optional - provider dependencies can become available during the execution of the app, so + /** Calling methods on the wrong thread */ + @JvmField + val INVALID_PROVIDER_ASSIGNMENT = + Issue.create( + id = "ProviderAssignment", + briefDescription = "Invalid use of Provider", + explanation = + """ + Ensures that results of `Provider.get()` are not stored in class fields. Doing \ + so may lead to bugs in the context of dynamic feature loading. Namely, optional \ + provider dependencies can become available during the execution of the app, so \ dependents must be ready to handle this situation. """, - category = Category.CORRECTNESS, - priority = 6, - severity = Severity.ERROR, - implementation = IMPLEMENTATION - ) - } + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } } diff --git a/tools/lint/src/main/kotlin/TasksMainThreadDetector.kt b/tools/lint/src/main/kotlin/TasksMainThreadDetector.kt new file mode 100644 index 00000000000..25dd5556abd --- /dev/null +++ b/tools/lint/src/main/kotlin/TasksMainThreadDetector.kt @@ -0,0 +1,113 @@ +// Copyright 2022 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.lint.checks + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter +import com.intellij.psi.util.InheritanceUtil +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.getParentOfType + +internal val GENERICS_PATTERN = Regex("<.*>") + +@Suppress("DetectorIsMissingAnnotations") +class TasksMainThreadDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List = + listOf( + "addOnSuccessListener", + "addOnFailureListener", + "addOnCompleteListener", + "addOnCanceledListener", + "continueWith", + "continueWithTask", + "onSuccessTask" + ) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isTaskMethod(method)) { + return + } + + // It's ok to call from a subclass of Task as it needs to implement overloads that don't take an + // executor. + val callingClass = node.getParentOfType() + if ( + callingClass != null && + InheritanceUtil.isInheritor(callingClass.javaPsi, "com.google.android.gms.tasks.Task") + ) { + val callingMethod = node.getParentOfType()?.javaPsi + if (method.isSameMethodAs(callingMethod)) { + return + } + } + + val firstArgument: PsiParameter = method.parameterList.parameters.firstOrNull() ?: return + if (!firstArgument.type.equalsToText("java.util.concurrent.Executor")) { + context.report( + TASK_MAIN_THREAD, + context.getCallLocation(node, includeReceiver = false, includeArguments = false), + "Use an Executor explicitly to avoid running on the main thread." + ) + } + } + + private fun PsiMethod.isSameMethodAs(other: PsiMethod?): Boolean = + other != null && + name == other.name && + parameterList.parameters.map { it.type.toString().replace(GENERICS_PATTERN, "") } == + other.parameterList.parameters.map { it.type.toString().replace(GENERICS_PATTERN, "") } + + private fun isTaskMethod(method: PsiMethod): Boolean { + (method.parent as? PsiClass)?.let { + return it.qualifiedName == "com.google.android.gms.tasks.Task" + } + return false + } + + companion object { + private val IMPLEMENTATION = + Implementation(TasksMainThreadDetector::class.java, Scope.JAVA_FILE_SCOPE) + + /** Calling methods on the wrong thread */ + @JvmField + val TASK_MAIN_THREAD = + Issue.create( + id = "TaskMainThread", + briefDescription = "Use an explicit Executor for Task continuations", + explanation = + """ + Not providing an executor results in continuations being executed on the main \ + thread, which in most cases is not intended. Please pass in an executor \ + explicitly. + """, + category = Category.PERFORMANCE, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } +} diff --git a/tools/lint/src/main/kotlin/ThreadPoolDetector.kt b/tools/lint/src/main/kotlin/ThreadPoolDetector.kt new file mode 100644 index 00000000000..a64996ad006 --- /dev/null +++ b/tools/lint/src/main/kotlin/ThreadPoolDetector.kt @@ -0,0 +1,124 @@ +// Copyright 2020 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.lint.checks + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +@Suppress("DetectorIsMissingAnnotations") +class ThreadPoolDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames(): List = + listOf( + "newCachedThreadPool", + "newFixedThreadPool", + "newScheduledThreadPool", + "newSingleThreadExecutor", + "newSingleThreadScheduledExecutor", + "newWorkStealingPool", + "factory" + ) + + override fun getApplicableConstructorTypes(): List = + listOf( + "java.lang.Thread", + "java.util.concurrent.ForkJoinPool", + "java.util.concurrent.ThreadPoolExecutor", + "java.util.concurrent.ScheduledThreadPoolExecutor", + "android.os.Handler" + ) + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + if (!isExecutorMethod(method) && !isPoolableFactory(method)) { + return + } + + context.report( + THREAD_POOL_CREATION, + context.getCallLocation(node, includeReceiver = false, includeArguments = true), + "Creating thread pools is not allowed" + ) + } + + override fun visitConstructor( + context: JavaContext, + node: UCallExpression, + constructor: PsiMethod + ) { + val cls = (constructor.parent as? PsiClass) ?: return + if (cls.qualifiedName == "android.os.Handler") { + if (node.valueArgumentCount == 0) return + if (node.valueArguments[0].toString().endsWith("getMainLooper()")) { + context.report( + THREAD_POOL_CREATION, + context.getCallLocation(node, includeReceiver = false, includeArguments = true), + "Creating Ui thread loopers is not allowed, use a `@UiThread Executor` instead" + ) + } + } else { + context.report( + THREAD_POOL_CREATION, + context.getCallLocation(node, includeReceiver = false, includeArguments = true), + "Creating threads or thread pools is not allowed" + ) + } + } + + private fun isExecutorMethod(method: PsiMethod): Boolean { + (method.parent as? PsiClass)?.let { + return it.qualifiedName == "java.util.concurrent.Executors" + } + return false + } + + private fun isPoolableFactory(method: PsiMethod): Boolean { + if (method.name != "factory") return false + (method.parent as? PsiClass)?.let { + return it.name == "PoolableExecutors" + } + return false + } + + companion object { + private val IMPLEMENTATION = + Implementation(ThreadPoolDetector::class.java, Scope.JAVA_FILE_SCOPE) + + /** Calling methods on the wrong thread */ + @JvmField + val THREAD_POOL_CREATION = + Issue.create( + id = "ThreadPoolCreation", + briefDescription = "Creating thread pools is not allowed", + explanation = + """ + Please use one of the executors provided by firebase-common. + + See: https://github.com/firebase/firebase-android-sdk/blob/master/docs/executors.md + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = IMPLEMENTATION + ) + } +} diff --git a/tools/lint/src/test/kotlin/AndroidxNullabilityTests.kt b/tools/lint/src/test/kotlin/AndroidxNullabilityTests.kt index f101e6c6875..a6f7203ea36 100644 --- a/tools/lint/src/test/kotlin/AndroidxNullabilityTests.kt +++ b/tools/lint/src/test/kotlin/AndroidxNullabilityTests.kt @@ -22,109 +22,123 @@ private const val EXPECTED_ERROR = "Use androidx nullability annotations" private const val NO_WARNINGS = "No warnings." private fun annotationSource(pkg: String, name: String): String { - return """ + return """ package $pkg; public @interface $name {} - """.trimIndent() + """ + .trimIndent() } private fun javaxAnnotation(name: String): String { - return annotationSource("javax.annotation", name) + return annotationSource("javax.annotation", name) } private fun androidxAnnotation(name: String): String { - return annotationSource("androidx.annotation", name) + return annotationSource("androidx.annotation", name) } -private val JAVAX_NULLABLE_CLASS = """ +private val JAVAX_NULLABLE_CLASS = + """ import javax.annotation.Nullable; @Nullable class Foo {} -""".trimIndent() +""" + .trimIndent() -private val ANDROIDX_NULLABLE_CLASS = """ +private val ANDROIDX_NULLABLE_CLASS = + """ import androidx.annotation.Nullable; @Nullable class Foo {} -""".trimIndent() +""" + .trimIndent() -private val JAVAX_NULLABLE_METHOD = """ +private val JAVAX_NULLABLE_METHOD = + """ import javax.annotation.Nullable; class Foo { @Nullable String hello() { return null; } } -""".trimIndent() +""" + .trimIndent() -private val ANDROIDX_NULLABLE_METHOD = """ +private val ANDROIDX_NULLABLE_METHOD = + """ import androidx.annotation.Nullable; class Foo { @Nullable String hello() { return null; } } -""".trimIndent() +""" + .trimIndent() -private val JAVAX_NON_NULL_METHOD_PARAMETER = """ +private val JAVAX_NON_NULL_METHOD_PARAMETER = + """ import javax.annotation.Nonnull; class Foo { String hello(@Nonnull String bar) { return null; } } -""".trimIndent() +""" + .trimIndent() -private val ANDROIDX_NON_NULL_METHOD_PARAMETER = """ +private val ANDROIDX_NON_NULL_METHOD_PARAMETER = + """ import androidx.annotation.NonNull; class Foo { String hello(@NonNull String bar) { return null; } } -""".trimIndent() +""" + .trimIndent() class AndroidxNullabilityTests : LintDetectorTest() { - override fun getDetector(): Detector = NonAndroidxNullabilityDetector() - - override fun getIssues(): MutableList = - mutableListOf(NonAndroidxNullabilityDetector.NON_ANDROIDX_NULLABILITY) - - fun testJavaxAnnotatedNullableClass() { - lint().files(java(JAVAX_NULLABLE_CLASS), java(javaxAnnotation("Nullable"))) - .run() - .checkContains(EXPECTED_ERROR) - } - - fun testAndroidxAnnotatedNullableClass() { - lint().files( - java(ANDROIDX_NULLABLE_CLASS), java(androidxAnnotation("Nullable"))) - .run() - .checkContains(NO_WARNINGS) - } - - fun testJavaxAnnotatedNullableMethod() { - lint().files( - java(JAVAX_NULLABLE_METHOD), java(javaxAnnotation("Nullable"))) - .run() - .checkContains(EXPECTED_ERROR) - } - - fun testAndroidxAnnotatedNullableMethod() { - lint().files( - java(ANDROIDX_NULLABLE_METHOD), java(androidxAnnotation("Nullable"))) - .run() - .checkContains(NO_WARNINGS) - } - - fun testJavaxAnnotatedNonNullMethodParameter() { - lint().files( - java(JAVAX_NON_NULL_METHOD_PARAMETER), java(javaxAnnotation("Nonnull"))) - .run() - .checkContains(EXPECTED_ERROR) - } - - fun testAndroidxAnnotatedNonNullMethodParameter() { - lint().files( - java(ANDROIDX_NON_NULL_METHOD_PARAMETER), java(androidxAnnotation("NonNull"))) - .run() - .checkContains(NO_WARNINGS) - } + override fun getDetector(): Detector = NonAndroidxNullabilityDetector() + + override fun getIssues(): MutableList = + mutableListOf(NonAndroidxNullabilityDetector.NON_ANDROIDX_NULLABILITY) + + fun testJavaxAnnotatedNullableClass() { + lint() + .files(java(JAVAX_NULLABLE_CLASS), java(javaxAnnotation("Nullable"))) + .run() + .checkContains(EXPECTED_ERROR) + } + + fun testAndroidxAnnotatedNullableClass() { + lint() + .files(java(ANDROIDX_NULLABLE_CLASS), java(androidxAnnotation("Nullable"))) + .run() + .checkContains(NO_WARNINGS) + } + + fun testJavaxAnnotatedNullableMethod() { + lint() + .files(java(JAVAX_NULLABLE_METHOD), java(javaxAnnotation("Nullable"))) + .run() + .checkContains(EXPECTED_ERROR) + } + + fun testAndroidxAnnotatedNullableMethod() { + lint() + .files(java(ANDROIDX_NULLABLE_METHOD), java(androidxAnnotation("Nullable"))) + .run() + .checkContains(NO_WARNINGS) + } + + fun testJavaxAnnotatedNonNullMethodParameter() { + lint() + .files(java(JAVAX_NON_NULL_METHOD_PARAMETER), java(javaxAnnotation("Nonnull"))) + .run() + .checkContains(EXPECTED_ERROR) + } + + fun testAndroidxAnnotatedNonNullMethodParameter() { + lint() + .files(java(ANDROIDX_NON_NULL_METHOD_PARAMETER), java(androidxAnnotation("NonNull"))) + .run() + .checkContains(NO_WARNINGS) + } } diff --git a/tools/lint/src/test/kotlin/DeferredApiDetectorTests.kt b/tools/lint/src/test/kotlin/DeferredApiDetectorTests.kt index bc206a1a4eb..a75d3b2da21 100644 --- a/tools/lint/src/test/kotlin/DeferredApiDetectorTests.kt +++ b/tools/lint/src/test/kotlin/DeferredApiDetectorTests.kt @@ -17,16 +17,17 @@ package com.google.firebase.lint.checks import com.android.tools.lint.checks.infrastructure.LintDetectorTest fun annotationSource(): String { - return """ + return """ package com.google.firebase.annotations; @Inherited public @interface DeferredApi {} - """.trimIndent() + """ + .trimIndent() } fun deferredSource(): String { - return """ + return """ package com.google.firebase.inject; import com.google.firebase.annotations.DeferredApi; @@ -38,52 +39,63 @@ fun deferredSource(): String { void whenAvailable(@NonNull DeferredHandler handler); } - """.trimIndent() + """ + .trimIndent() } fun providerSource(): String { - return """ + return """ package com.google.firebase.inject; public interface Provider { T get(); } - """.trimIndent() + """ + .trimIndent() } -fun annotatedInterface() = """ +fun annotatedInterface() = + """ import com.google.firebase.annotations.DeferredApi; interface MyApi { @DeferredApi void myMethod(); } - """.trimIndent() + """ + .trimIndent() class DeferredApiDetectorTests : LintDetectorTest() { - override fun getDetector() = DeferredApiDetector() + override fun getDetector() = DeferredApiDetector() - override fun getIssues() = mutableListOf(DeferredApiDetector.INVALID_DEFERRED_API_USE) + override fun getIssues() = mutableListOf(DeferredApiDetector.INVALID_DEFERRED_API_USE) - fun test_directUseOfDeferredApiMethodInConstructor_shouldFail() { - lint().files( - java(annotationSource()), - java(annotatedInterface()), - java(""" + fun test_directUseOfDeferredApiMethodInConstructor_shouldFail() { + lint() + .files( + java(annotationSource()), + java(annotatedInterface()), + java( + """ class UseOfMyApi { UseOfMyApi(MyApi api) { api.myMethod(); } } - """.trimIndent())) - .run() - .checkContains("myMethod is only safe to call") - } - - fun test_directUseOfDeferredApiMethodInMethodThroughField_shouldFail() { - lint().files( - java(annotationSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .checkContains("myMethod is only safe to call") + } + + fun test_directUseOfDeferredApiMethodInMethodThroughField_shouldFail() { + lint() + .files( + java(annotationSource()), + java(annotatedInterface()), + java( + """ class UseOfMyApi { private final MyApi api; UseOfMyApi(MyApi api) { @@ -94,18 +106,23 @@ class DeferredApiDetectorTests : LintDetectorTest() { api.myMethod(); } } - """.trimIndent())) - .run() - .checkContains("myMethod is only safe to call") - } - - fun test_whenAvailableUseOfDeferredApiMethodThroughFieldInLambda_shouldSucceed() { - lint().files( - java(annotationSource()), - java(deferredSource()), - java(providerSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .checkContains("myMethod is only safe to call") + } + + fun test_whenAvailableUseOfDeferredApiMethodThroughFieldInLambda_shouldSucceed() { + lint() + .files( + java(annotationSource()), + java(deferredSource()), + java(providerSource()), + java(annotatedInterface()), + java( + """ import com.google.firebase.inject.Deferred; class UseOfMyApi { private final Deferred api; @@ -117,18 +134,23 @@ class DeferredApiDetectorTests : LintDetectorTest() { apiProvider.get().myMethod()); } } - """.trimIndent())) - .run() - .expectClean() - } - - fun test_whenAvailableUseOfDeferredApiMethodThroughFieldInAnonymousClass_shouldSucceed() { - lint().files( - java(annotationSource()), - java(deferredSource()), - java(providerSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_whenAvailableUseOfDeferredApiMethodThroughFieldInAnonymousClass_shouldSucceed() { + lint() + .files( + java(annotationSource()), + java(deferredSource()), + java(providerSource()), + java(annotatedInterface()), + java( + """ import com.google.firebase.inject.Deferred; import com.google.firebase.inject.Provider; class UseOfMyApi { @@ -144,18 +166,23 @@ class DeferredApiDetectorTests : LintDetectorTest() { }); } } - """.trimIndent())) - .run() - .expectClean() - } - - fun test_whenAvailableUseOfDeferredApiMethodInConstructorInLambda_shouldSucceed() { - lint().files( - java(annotationSource()), - java(deferredSource()), - java(providerSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_whenAvailableUseOfDeferredApiMethodInConstructorInLambda_shouldSucceed() { + lint() + .files( + java(annotationSource()), + java(deferredSource()), + java(providerSource()), + java(annotatedInterface()), + java( + """ import com.google.firebase.inject.Deferred; class UseOfMyApi { UseOfMyApi(Deferred api) { @@ -163,18 +190,23 @@ class DeferredApiDetectorTests : LintDetectorTest() { apiProvider.get().myMethod()); } } - """.trimIndent())) - .run() - .expectClean() - } - - fun test_whenAvailableUseOfDeferredApiMethodThatCallsToAnotherDeferredApi_shouldSucceed() { - lint().files( - java(annotationSource()), - java(deferredSource()), - java(providerSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_whenAvailableUseOfDeferredApiMethodThatCallsToAnotherDeferredApi_shouldSucceed() { + lint() + .files( + java(annotationSource()), + java(deferredSource()), + java(providerSource()), + java(annotatedInterface()), + java( + """ import com.google.firebase.annotations.DeferredApi; import com.google.firebase.inject.Deferred; class UseOfMyApi { @@ -189,18 +221,23 @@ class DeferredApiDetectorTests : LintDetectorTest() { api.myMethod(); } } - """.trimIndent())) - .run() - .expectClean() - } - - fun test_whenAvailableUseOfRegularMethodThatCallsToAnotherDeferredApi_shouldFail() { - lint().files( - java(annotationSource()), - java(deferredSource()), - java(providerSource()), - java(annotatedInterface()), - java(""" + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_whenAvailableUseOfRegularMethodThatCallsToAnotherDeferredApi_shouldFail() { + lint() + .files( + java(annotationSource()), + java(deferredSource()), + java(providerSource()), + java(annotatedInterface()), + java( + """ import com.google.firebase.annotations.DeferredApi; import com.google.firebase.inject.Deferred; class UseOfMyApi { @@ -214,8 +251,11 @@ class DeferredApiDetectorTests : LintDetectorTest() { api.myMethod(); } } - """.trimIndent())) - .run() - .checkContains("myMethod is only safe to call") - } + """ + .trimIndent() + ) + ) + .run() + .checkContains("myMethod is only safe to call") + } } diff --git a/tools/lint/src/test/kotlin/FirebaseAppGetDetectorTests.kt b/tools/lint/src/test/kotlin/FirebaseAppGetDetectorTests.kt new file mode 100644 index 00000000000..97c4b7e10fd --- /dev/null +++ b/tools/lint/src/test/kotlin/FirebaseAppGetDetectorTests.kt @@ -0,0 +1,180 @@ +// Copyright 2022 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.lint.checks + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +private val FIREBASE_APP = + """ + package com.google.firebase; + + public class FirebaseApp { + public T get(Class t) { + return null; + } + + public static FirebaseApp getInstance() { + return null; + } + } +""" + .trimIndent() + +class FirebaseAppGetDetectorTests : LintDetectorTest() { + override fun getDetector(): Detector = FirebaseAppGetDetector() + + override fun getIssues(): MutableList = mutableListOf(FirebaseAppGetDetector.ISSUE) + + fun test_app_get_from_getInstance_shouldNotFail() { + lint() + .files( + java(FIREBASE_APP), + java( + """ + import com.google.firebase.FirebaseApp; + + public class Foo { + public static Foo getInstance(FirebaseApp app) { + return app.get(Foo.class); + } + } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_app_get_from_getInstance_returningSubclass_shouldNotFail() { + lint() + .files( + java(FIREBASE_APP), + java( + """ + import com.google.firebase.FirebaseApp; + + public class Foo { + public static FooImpl getInstance(FirebaseApp app) { + return app.get(FooImpl.class); + } + } + class FooImpl extends Foo {} + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_app_get_from_getInstance_withWrongReturnType_shouldFail() { + lint() + .files( + java(FIREBASE_APP), + java( + """ + import com.google.firebase.FirebaseApp; + + public class Foo { + public static String getInstance(FirebaseApp app) { + app.get(Foo.class); + return ""; + } + } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Use of FirebaseApp#get(Class) is discouraged") + } + + fun test_app_get_from_getInstance_shouldNotFail_kotlin() { + lint() + .files( + java(FIREBASE_APP), + kotlin( + """ + import com.google.firebase.FirebaseApp + + class Foo { + companion object { + @JvmStatic + val instance = FirebaseApp.getInstance().get(Foo::class.java) + + @JvmStatic + fun getInstance(app: FirebaseApp) = app.get(Foo::class.java) + } + } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_app_get_from_getInstance_returningSubclass_shouldNotFail_kotlin() { + lint() + .files( + java(FIREBASE_APP), + kotlin( + """ + import com.google.firebase.FirebaseApp + + class Foo { + companion object { + @JvmStatic + val instance = FirebaseApp.getInstance().get(FooImpl::class.java) + + @JvmStatic + fun getInstance(app: FirebaseApp) = app.get(FooImpl::class.java) + } + } + class FooImpl : Foo() + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_app_get_from_getInstance_withWrongReturnType_shouldFail_kotlin() { + lint() + .files( + java(FIREBASE_APP), + kotlin( + """ + import com.google.firebase.FirebaseApp; + + class Foo { + companion object { + + @JvmStatic + fun getInstance(app: FirebaseApp) = "".also { app.get(Foo::class.java) } + } + } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Use of FirebaseApp#get(Class) is discouraged") + } +} diff --git a/tools/lint/src/test/kotlin/ManifestTests.kt b/tools/lint/src/test/kotlin/ManifestTests.kt index c790de20101..8ab8e7e2e3f 100644 --- a/tools/lint/src/test/kotlin/ManifestTests.kt +++ b/tools/lint/src/test/kotlin/ManifestTests.kt @@ -23,12 +23,14 @@ import com.google.firebase.lint.checks.ManifestElementHasNoExportedAttributeDete import java.lang.AssertionError internal fun manifestWith(cmp: Component, exported: Boolean? = null): String { - val exportedStr = when (exported) { - true, false -> "android:exported=\"$exported\"" - null -> "" + val exportedStr = + when (exported) { + true, + false -> "android:exported=\"$exported\"" + null -> "" } - return """ + return """ @@ -36,46 +38,44 @@ internal fun manifestWith(cmp: Component, exported: Boolean? = null): String { <${cmp.xmlName} android:name="foo" $exportedStr/> - """.trimIndent() + """ + .trimIndent() } fun TestLintResult.checkContains(expected: String) { - this.check(TestResultChecker { output -> - if (!output.contains(expected)) { - throw AssertionError("Expected:<[$expected]>, but was:<[$output]>") - } - }) + this.check( + TestResultChecker { output -> + if (!output.contains(expected)) { + throw AssertionError("Expected:<[$expected]>, but was:<[$output]>") + } + } + ) } class Test : LintDetectorTest() { - fun testComponents_withNoExportedAttr_shouldFail() { - Component.values().forEach { - lint().files( - manifest(manifestWith(it))) - .run() - .checkContains("Error: Set android:exported attribute explicitly") - } + fun testComponents_withNoExportedAttr_shouldFail() { + Component.values().forEach { + lint() + .files(manifest(manifestWith(it))) + .run() + .checkContains("Error: Set android:exported attribute explicitly") } + } - fun testComponents_withExportedAttrTrue_shouldSucceed() { - Component.values().forEach { - lint().files( - manifest(manifestWith(it, true))) - .run() - .expectClean() - } + fun testComponents_withExportedAttrTrue_shouldSucceed() { + Component.values().forEach { + lint().files(manifest(manifestWith(it, true))).run().expectClean() } + } - fun testComponents_withExportedAttrFalse_shouldSucceed() { - Component.values().forEach { - lint().files( - manifest(manifestWith(it, false))) - .run() - .expectClean() - } + fun testComponents_withExportedAttrFalse_shouldSucceed() { + Component.values().forEach { + lint().files(manifest(manifestWith(it, false))).run().expectClean() } + } - override fun getDetector(): Detector = ManifestElementHasNoExportedAttributeDetector() + override fun getDetector(): Detector = ManifestElementHasNoExportedAttributeDetector() - override fun getIssues(): MutableList = mutableListOf(ManifestElementHasNoExportedAttributeDetector.EXPORTED_MISSING_ISSUE) + override fun getIssues(): MutableList = + mutableListOf(ManifestElementHasNoExportedAttributeDetector.EXPORTED_MISSING_ISSUE) } diff --git a/tools/lint/src/test/kotlin/ProviderAssignmentDetectorTests.kt b/tools/lint/src/test/kotlin/ProviderAssignmentDetectorTests.kt index 9c5aac13ade..fbdb20ee020 100644 --- a/tools/lint/src/test/kotlin/ProviderAssignmentDetectorTests.kt +++ b/tools/lint/src/test/kotlin/ProviderAssignmentDetectorTests.kt @@ -17,13 +17,16 @@ package com.google.firebase.lint.checks import com.android.tools.lint.checks.infrastructure.LintDetectorTest class ProviderAssignmentDetectorTests : LintDetectorTest() { - override fun getDetector() = ProviderAssignmentDetector() + override fun getDetector() = ProviderAssignmentDetector() - override fun getIssues() = mutableListOf( - ProviderAssignmentDetector.INVALID_PROVIDER_ASSIGNMENT) + override fun getIssues() = mutableListOf(ProviderAssignmentDetector.INVALID_PROVIDER_ASSIGNMENT) - fun test_assignmentToClassField_shouldFail() { - lint().files(java(providerSource()), java(""" + fun test_assignmentToClassField_shouldFail() { + lint() + .files( + java(providerSource()), + java( + """ import com.google.firebase.inject.Provider; class Foo { @@ -32,13 +35,20 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { this.value = p.get(); } } - """.trimIndent())) - .run() - .checkContains("Provider.get() assignment") - } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Provider.get() assignment") + } - fun test_assignmentAndUseOfProvider_shouldSucceed() { - lint().files(java(providerSource()), java(""" + fun test_assignmentAndUseOfProvider_shouldSucceed() { + lint() + .files( + java(providerSource()), + java( + """ import com.google.firebase.inject.Provider; class Foo { @@ -50,13 +60,20 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { String value = p.get(); } } - """.trimIndent())) - .run() - .expectClean() - } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } - fun test_assignmentFromAStoredProvider_shouldFail() { - lint().files(java(providerSource()), java(""" + fun test_assignmentFromAStoredProvider_shouldFail() { + lint() + .files( + java(providerSource()), + java( + """ import com.google.firebase.inject.Provider; class Foo { @@ -69,13 +86,20 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { value = p.get(); } } - """.trimIndent())) - .run() - .checkContains("Provider.get() assignment") - } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Provider.get() assignment") + } - fun test_assignmentToLocalVariable_shouldSucceed() { - lint().files(java(providerSource()), java(""" + fun test_assignmentToLocalVariable_shouldSucceed() { + lint() + .files( + java(providerSource()), + java( + """ import com.google.firebase.inject.Provider; class Foo { @@ -83,13 +107,20 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { String value = p.get(); } } - """.trimIndent())) - .run() - .expectClean() - } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } - fun test_assignmentOfAPropertyOrMethodOfTheProvidedObject_shouldSucceed() { - lint().files(java(providerSource()), java(""" + fun test_assignmentOfAPropertyOrMethodOfTheProvidedObject_shouldSucceed() { + lint() + .files( + java(providerSource()), + java( + """ import com.google.firebase.inject.Provider; class Foo { @@ -100,17 +131,22 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { s = p.get().toString(); } } - """.trimIndent())) - .run() - .expectClean() - } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } - fun test_assignmentFromWithinADeferredApiMethod_shouldSucceed() { - lint().files( - java(providerSource()), - java(annotationSource()), - java(deferredSource()), - java(""" + fun test_assignmentFromWithinADeferredApiMethod_shouldSucceed() { + lint() + .files( + java(providerSource()), + java(annotationSource()), + java(deferredSource()), + java( + """ import com.google.firebase.inject.Deferred; class Foo { private String s; @@ -120,8 +156,11 @@ class ProviderAssignmentDetectorTests : LintDetectorTest() { }); } } - """.trimIndent())) - .run() - .expectClean() - } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } } diff --git a/tools/lint/src/test/kotlin/TasksMainThreadDetectorTests.kt b/tools/lint/src/test/kotlin/TasksMainThreadDetectorTests.kt new file mode 100644 index 00000000000..45acd7f0b6b --- /dev/null +++ b/tools/lint/src/test/kotlin/TasksMainThreadDetectorTests.kt @@ -0,0 +1,139 @@ +// Copyright 2022 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.lint.checks + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +fun taskSource(): String { + return """ + package com.google.android.gms.tasks; + + import java.util.concurrent.Executor; + + public interface Task { + Task continueWith(Executor executor, String dummy); + Task continueWith(String dummy); + } + """ + .trimIndent() +} + +class TasksMainThreadDetectorTests : LintDetectorTest() { + override fun getDetector(): Detector = TasksMainThreadDetector() + + override fun getIssues(): MutableList = + mutableListOf(TasksMainThreadDetector.TASK_MAIN_THREAD) + + fun test_continueWith_withoutExecutor_shouldFail() { + lint() + .files( + java(taskSource()), + java( + """ + import com.google.android.gms.tasks.Task; + class Hello { + public static Task useTask(Task task) { + return task.continueWith("hello"); + } + } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Use an Executor explicitly to avoid running on the main thread") + } + + fun test_continueWith_withExecutor_shouldSucceed() { + lint() + .files( + java(taskSource()), + java( + """ + import com.google.android.gms.tasks.Task; + import java.util.concurrent.Executor; + class Hello { + public static Task useTask(Executor executor, Task task) { + return task.continueWith(executor, "hello"); + } + } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_continueWith_withoutExecutor_whileImplementingOverload_shouldSucceed() { + lint() + .files( + java(taskSource()), + java( + """ + import com.google.android.gms.tasks.Task; + import java.util.concurrent.Executor; + class Hello implements Task { + private final Task delegate; + + Hello(Task delegate) { this.delegate = delegate;} + + @Override + public Task continueWith(Executor executor, String dummy) { + return delegate.continueWith(executor, dummy); + } + public Task continueWith(String dummy) { + return delegate.continueWith(dummy); + } + } + """ + .trimIndent() + ) + ) + .run() + .expectClean() + } + + fun test_continueWith_withoutExecutor_fromWrongOverload_shouldFail() { + lint() + .files( + java(taskSource()), + java( + """ + import com.google.android.gms.tasks.Task; + import java.util.concurrent.Executor; + class Hello implements Task { + private final Task delegate; + + Hello(Task delegate) { this.delegate = delegate;} + + @Override + public Task continueWith(Executor executor, String dummy) { + return delegate.continueWith(dummy); + } + public Task continueWith(String dummy) { + return delegate.continueWith(dummy); + } + } + """ + .trimIndent() + ) + ) + .run() + .checkContains("Use an Executor explicitly to avoid running on the main thread") + } +} diff --git a/transport/transport-backend-cct/gradle.properties b/transport/transport-backend-cct/gradle.properties index 0c32b609ca6..74fa8f92932 100644 --- a/transport/transport-backend-cct/gradle.properties +++ b/transport/transport-backend-cct/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=3.1.8 -latestReleasedVersion=3.1.7 +version=3.1.9 +latestReleasedVersion=3.1.8 diff --git a/transport/transport-runtime/gradle.properties b/transport/transport-runtime/gradle.properties index dda29bf0493..173c7edf124 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=3.1.8 -latestReleasedVersion=3.1.7 +version=3.1.9 +latestReleasedVersion=3.1.8 android.enableUnitTestBinaryResources=true diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ExecutionModule.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ExecutionModule.java index 64eeae96a48..17643e1ee35 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ExecutionModule.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ExecutionModule.java @@ -14,6 +14,7 @@ package com.google.android.datatransport.runtime; +import android.annotation.SuppressLint; import dagger.Module; import dagger.Provides; import java.util.concurrent.Executor; @@ -24,6 +25,7 @@ abstract class ExecutionModule { @Singleton @Provides + @SuppressLint("ThreadPoolCreation") static Executor executor() { return new SafeLoggingExecutor(Executors.newSingleThreadExecutor()); } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ForcedSender.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ForcedSender.java index d0a9ef2b8cc..9bc141fed41 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ForcedSender.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/ForcedSender.java @@ -19,23 +19,24 @@ import androidx.annotation.WorkerThread; import com.google.android.datatransport.Priority; import com.google.android.datatransport.Transport; +import com.google.android.datatransport.runtime.logging.Logging; @Discouraged( message = "TransportRuntime is not a realtime delivery system, don't use unless you absolutely must.") public final class ForcedSender { + private static final String LOG_TAG = "ForcedSender"; + + @SuppressLint("DiscouragedApi") @WorkerThread public static void sendBlocking(Transport transport, Priority priority) { - @SuppressLint("DiscouragedApi") - TransportContext context = getTransportContextOrThrow(transport).withPriority(priority); - TransportRuntime.getInstance().getUploader().logAndUpdateState(context, 1); - } - - private static TransportContext getTransportContextOrThrow(Transport transport) { if (transport instanceof TransportImpl) { - return ((TransportImpl) transport).getTransportContext(); + TransportContext context = + ((TransportImpl) transport).getTransportContext().withPriority(priority); + TransportRuntime.getInstance().getUploader().logAndUpdateState(context, 1); + } else { + Logging.w(LOG_TAG, "Expected instance of `TransportImpl`, got `%s`.", transport); } - throw new IllegalArgumentException("Expected instance of TransportImpl."); } private ForcedSender() {} diff --git a/transport/transport-runtime/transport-runtime.gradle b/transport/transport-runtime/transport-runtime.gradle index 2272e16ba95..334959a4f1a 100644 --- a/transport/transport-runtime/transport-runtime.gradle +++ b/transport/transport-runtime/transport-runtime.gradle @@ -67,6 +67,12 @@ firebaseLibrary { } } +vendor { + // Integration tests use dagger classes that are not used in the main SDK, + // so we disable dead code elimination to ensure those classes are preserved. + optimize = false +} + android { compileSdkVersion project.targetSdkVersion defaultConfig {