diff --git a/.gemini/config.yaml b/.gemini/config.yaml new file mode 100644 index 00000000000..cf6e5d0be1f --- /dev/null +++ b/.gemini/config.yaml @@ -0,0 +1,9 @@ +have_fun: false +code_review: + disable: false + comment_severity_threshold: LOW + max_review_comments: -1 + pull_request_opened: + help: true + summary: false + code_review: true diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index afd55e8dee6..7e1210084f4 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -3,37 +3,35 @@ name: ⚠️ Report a Bug about: Think you found a bug in the SDK? Report it here. --- - ### [READ] Step 1: Are you in the right place? -Issues filed here should be about bugs in __the code in this repository__. -If you have a general question, need help debugging, or fall into some -other category use one of these other channels: +Issues filed here should be about bugs in **the code in this repository**. If you have a general +question, need help debugging, or fall into some other category use one of these other channels: - * For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) - with the firebase tag. - * For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) - google group. - * For help troubleshooting your application that does not fall under one - of the above categories, reach out to the personalized - [Firebase support channel](https://firebase.google.com/support/). +- For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) + with the firebase tag. +- For general Firebase discussion, use the + [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) google group. +- For help troubleshooting your application that does not fall under one of the above categories, + reach out to the personalized [Firebase support channel](https://firebase.google.com/support/). ### [REQUIRED] Step 2: Describe your environment - * Android Studio version: _____ - * Firebase Component: _____ (Database, Firestore, Storage, Functions, etc) - * Component version: _____ +- Android Studio version: **\_** +- Firebase Component: **\_** (Database, Firestore, Storage, Functions, etc) +- Component version: **\_** ### [REQUIRED] Step 3: Describe the problem #### Steps to reproduce: -What happened? How can we make the problem occur? -This could be a description, log/console output, etc. +What happened? How can we make the problem occur? This could be a description, log/console output, +etc. #### Relevant Code: diff --git a/.github/ISSUE_TEMPLATE/fr.md b/.github/ISSUE_TEMPLATE/fr.md index cdd3d6922ba..1989c975964 100644 --- a/.github/ISSUE_TEMPLATE/fr.md +++ b/.github/ISSUE_TEMPLATE/fr.md @@ -3,7 +3,7 @@ name: 💡 Feature Request about: Have a feature you'd like to see in the Android SDK? Request it here. --- - diff --git a/.github/workflows/ai-daily-tests.yml b/.github/workflows/ai-daily-tests.yml new file mode 100644 index 00000000000..7d8e3842470 --- /dev/null +++ b/.github/workflows/ai-daily-tests.yml @@ -0,0 +1,68 @@ +name: Firebase AI Daily Tests + +on: + schedule: + - cron: 2 7 * * * # Runs automatically once a day + workflow_dispatch: # Allow triggering the workflow manually + +permissions: + contents: read + +jobs: + dailies: + name: "Daily Tests" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: true + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Set up JDK 17 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json + - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 + + - name: Run tests + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 + env: + FIREBASE_CI: 1 + FTL_RESULTS_BUCKET: android-ci + FTL_RESULTS_DIR: ${{ format('logs/{0}/{1}_{2}/artifacts/', github.workflow, github.run_id, github.run_attempt) }} + FIREBASE_APP_CHECK_DEBUG_SECRET: ${{ secrets.FIREBASE_APP_CHECK_DEBUG_SECRET }} + with: + api-level: 34 + arch: x86_64 + ram-size: 4096M + heap-size: 4096M + script: | + adb logcat -v time > logcat.txt & + ./gradlew firebase-ai:connectedCheck withErrorProne -PtargetBackend="prod" + + - name: Upload logs + if: failure() + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: logcat.txt + path: logcat.txt + retention-days: 7 + if-no-files-found: ignore diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 60660863235..14b685c8e2d 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -17,9 +17,9 @@ jobs: with: fetch-depth: 100 submodules: true - - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0 + - uses: ruby/setup-ruby@0481980f17b760ef6bca5e8c55809102a0af1e5a # v1.263.0 with: - ruby-version: '2.7' + ruby-version: '3.4' - name: Setup Bundler run: ./ci/danger/setup_bundler.sh - name: Danger CHANGELOG verifier diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index 83fdc3ec605..f09f278210a 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -9,44 +9,13 @@ on: - main jobs: - determine_changed: - name: "Determine changed modules" - runs-on: ubuntu-22.04 - if: (github.repository == 'Firebase/firebase-android-sdk' && github.event_name == 'push') || github.event_name == 'pull_request' - outputs: - modules: ${{ steps.changed-modules.outputs.modules }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 2 - submodules: true - - - name: Set up JDK 17 - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 - with: - java-version: 17 - distribution: temurin - cache: gradle - - - 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 - echo modules=$(cat modules.json) >> $GITHUB_OUTPUT - check_format: name: "Check Format" runs-on: ubuntu-22.04 - needs: - - determine_changed - strategy: - fail-fast: false - matrix: - module: ${{ fromJSON(needs.determine_changed.outputs.modules) }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - fetch-depth: 2 submodules: true - name: Set up JDK 17 @@ -56,18 +25,5 @@ jobs: distribution: temurin cache: gradle - - name: ${{ matrix.module }} Check Format - run: | - ./gradlew ${{matrix.module}}:spotlessCheck - - # A job that fails if any job in the check_format matrix fails, - # to be used as a required check for merging. - check_all: - runs-on: ubuntu-22.04 - if: always() - name: Check Format (matrix) - needs: check_format - steps: - - name: Check matrix - if: needs.check_format.result != 'success' - run: exit 1 + - name: Run Spotless + run: ./gradlew ${{matrix.module}}:spotlessCheck diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 487d0229d3b..f3765005c81 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -38,6 +38,7 @@ jobs: runs-on: ubuntu-22.04 needs: - determine_changed + if: ${{ needs.determine_changed.outputs.modules != '[]' }} strategy: fail-fast: false matrix: @@ -56,10 +57,6 @@ jobs: distribution: temurin cache: gradle - - name: Clone vertexai mock responses - if: matrix.module == ':firebase-vertexai' - run: firebase-vertexai/update_responses.sh - - name: Clone ai mock responses if: matrix.module == ':firebase-ai' run: firebase-ai/update_responses.sh @@ -67,6 +64,7 @@ jobs: - name: Add google-services.json env: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + if: env.INTEG_TESTS_GOOGLE_SERVICES != '' && env.INTEG_TESTS_GOOGLE_SERVICES != 'null' run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json @@ -76,6 +74,7 @@ jobs: run: | ./gradlew ${{matrix.module}}:check withErrorProne - name: Compute upload file name + if: always() run: | MODULE=${{matrix.module}} echo "ARTIFACT_NAME=${MODULE//:/_}" >> $GITHUB_ENV @@ -92,12 +91,11 @@ jobs: # to be used as a required check for merging. check_all: runs-on: ubuntu-22.04 - if: always() name: Unit Tests (matrix) needs: unit_tests + if: ${{ failure() }} steps: - - name: Check test matrix - if: needs.unit_tests.result != 'success' + - name: Check test matrix results run: exit 1 @@ -132,6 +130,7 @@ jobs: - name: Add google-services.json env: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + if: env.INTEG_TESTS_GOOGLE_SERVICES != '' && env.INTEG_TESTS_GOOGLE_SERVICES != 'null' run: | echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > google-services.json - uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 @@ -169,5 +168,8 @@ jobs: - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 + if: always() with: files: "artifacts/**/*.xml" + comment_mode: off + compare_to_earlier_commit: false diff --git a/.github/workflows/dataconnect.yml b/.github/workflows/dataconnect.yml index 2808b19e7d6..0984226dbfc 100644 --- a/.github/workflows/dataconnect.yml +++ b/.github/workflows/dataconnect.yml @@ -13,6 +13,15 @@ on: pull_request: paths: - .github/workflows/dataconnect.yml + - gradlew + - build.gradle.kts + - gradle.properties + - gradlew.bat + - settings.gradle.kts + - subprojects.cfg + - 'gradle/**' + - 'plugins/**' + - '!plugins/**/*.md' - 'firebase-dataconnect/**' - '!firebase-dataconnect/demo/**' - '!firebase-dataconnect/scripts/**' @@ -25,7 +34,7 @@ env: FDC_JAVA_VERSION: ${{ inputs.javaVersion || '17' }} FDC_ANDROID_EMULATOR_API_LEVEL: ${{ inputs.androidEmulatorApiLevel || '34' }} FDC_NODEJS_VERSION: ${{ inputs.nodeJsVersion || '20' }} - FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.5.1' }} + FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.18.0' }} FDC_FIREBASE_TOOLS_DIR: /tmp/firebase-tools FDC_FIREBASE_COMMAND: /tmp/firebase-tools/node_modules/.bin/firebase FDC_PYTHON_VERSION: ${{ inputs.pythonVersion || '3.13' }} @@ -125,6 +134,7 @@ jobs: ./gradlew \ --profile \ + --warning-mode all \ ${{ (inputs.gradleInfoLog && '--info') || '' }} \ :firebase-dataconnect:assembleDebugAndroidTest @@ -213,7 +223,7 @@ jobs: emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true script: | - set -eux && ./gradlew ${{ (inputs.gradleInfoLog && '--info') || '' }} :firebase-dataconnect:connectedCheck :firebase-dataconnect:connectors:connectedCheck + set -eux && ./gradlew --warning-mode all ${{ (inputs.gradleInfoLog && '--info') || '' }} :firebase-dataconnect:connectedCheck :firebase-dataconnect:connectors:connectedCheck - name: Upload Log Files uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 @@ -262,6 +272,17 @@ jobs: with: args: -color /github/workspace/.github/workflows/dataconnect.yml + shellcheck: + continue-on-error: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: false + sparse-checkout: 'firebase-dataconnect/' + - name: shellcheck + run: find . -name '*.sh' -print0 | xargs --verbose -0 shellcheck --norc --enable=all --shell=bash + python-ci-unit-tests: continue-on-error: false runs-on: ubuntu-latest diff --git a/.github/workflows/dataconnect_demo_app.yml b/.github/workflows/dataconnect_demo_app.yml index 24a5434bd57..b8e65181792 100644 --- a/.github/workflows/dataconnect_demo_app.yml +++ b/.github/workflows/dataconnect_demo_app.yml @@ -18,7 +18,7 @@ on: env: FDC_NODE_VERSION: ${{ inputs.nodeVersion || '20' }} - FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.5.1' }} + FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.18.0' }} FDC_JAVA_VERSION: ${{ inputs.javaVersion || '17' }} FDC_FIREBASE_TOOLS_DIR: ${{ github.workspace }}/firebase-tools FDC_FIREBASE_COMMAND: ${{ github.workspace }}/firebase-tools/node_modules/.bin/firebase @@ -106,12 +106,21 @@ jobs: set -x firebase-dataconnect/demo/gradlew \ --project-dir firebase-dataconnect/demo \ - --no-daemon \ ${{ (inputs.gradleInfoLog && '--info') || '' }} \ --profile \ - -PdataConnect.minimalApp.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \ + --warning-mode all \ + -PdataConnect.demo.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \ assemble test + - name: gradle dokkaGeneratePublicationHtml + run: | + set -x + firebase-dataconnect/demo/gradlew \ + --warning-mode all \ + --project-dir firebase-dataconnect/demo \ + ${{ (inputs.gradleInfoLog && '--info') || '' }} \ + dokkaGeneratePublicationHtml + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: apks @@ -126,6 +135,13 @@ jobs: if-no-files-found: warn compression-level: 9 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: ktdoc + path: firebase-dataconnect/demo/build/dokka/html + if-no-files-found: warn + compression-level: 9 + spotlessCheck: continue-on-error: false runs-on: ubuntu-latest @@ -162,6 +178,7 @@ jobs: run: | set -x firebase-dataconnect/demo/gradlew \ + --warning-mode all \ --project-dir firebase-dataconnect/demo \ --no-daemon \ ${{ (inputs.gradleInfoLog && '--info') || '' }} \ diff --git a/.github/workflows/firestore_ci_tests.yml b/.github/workflows/firestore_ci_tests.yml index a7ea11b1624..3828d85aea3 100644 --- a/.github/workflows/firestore_ci_tests.yml +++ b/.github/workflows/firestore_ci_tests.yml @@ -250,11 +250,85 @@ jobs: retention-days: 7 if-no-files-found: ignore + firestore_emulator_integ_tests: + name: "System Tests Against Emulator" + runs-on: ubuntu-latest + needs: + - determine_changed + # only run on post submit or PRs not originating from forks. + 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)) && contains(fromJSON(needs.determine_changed.outputs.modules), ':firebase-firestore') + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 2 + submodules: true + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Add google-services.json + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES | base64 -d > firebase-firestore/google-services.json + + - name: Set up JDK 21 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 21 + distribution: temurin + cache: gradle + + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4 + + - name: Start Emulator + env: + EXPERIMENTAL_MODE: true + run: | + gcloud emulators firestore start --host-port=127.0.0.1:8080 --quiet & + + - name: Set up JDK 17 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + java-version: 17 + distribution: temurin + cache: gradle + + - name: Firestore Emulator Integ Tests + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #v2.33.0 + env: + FIREBASE_CI: 1 + FTL_RESULTS_BUCKET: android-ci + FTL_RESULTS_DIR: ${{ github.event_name == 'pull_request' && format('pr-logs/pull/{0}/{1}/{2}/{3}_{4}/artifacts/', github.repository, github.event.pull_request.number, github.job, github.run_id, github.run_attempt) || format('logs/{0}/{1}_{2}/artifacts/', github.workflow, github.run_id, github.run_attempt)}} + FIREBASE_APP_CHECK_DEBUG_SECRET: ${{ secrets.FIREBASE_APP_CHECK_DEBUG_SECRET }} + with: + api-level: 31 + arch: x86_64 + ram-size: 4096M + heap-size: 4096M + script: | + adb logcat -v time > logcat.txt & + ./gradlew firebase-firestore:connectedCheck withErrorProne -PtargetBackend="emulator" + - name: Upload logs + if: failure() + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: emulator-logcat.txt + path: logcat.txt + retention-days: 7 + if-no-files-found: ignore + check-required-tests: runs-on: ubuntu-latest if: always() name: Check all required Firestore tests results - needs: [integ_tests, named_integ_tests] + needs: [firestore_emulator_integ_tests] steps: - name: Check test matrix if: needs.integ_tests.result == 'failure' || needs.named_integ_tests.result == 'failure' diff --git a/.github/workflows/metalava-semver-check.yml b/.github/workflows/metalava-semver-check.yml index 0ec7f35e49c..04a3e7ef57c 100644 --- a/.github/workflows/metalava-semver-check.yml +++ b/.github/workflows/metalava-semver-check.yml @@ -2,6 +2,8 @@ name: Metalava SemVer Check on: pull_request: + branches: + - main jobs: semver-check: @@ -9,8 +11,10 @@ jobs: permissions: pull-requests: write steps: - - name: Checkout PR + - name: Checkout main uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.base_ref }} - name: Set up JDK 17 uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 @@ -19,13 +23,13 @@ jobs: distribution: temurin cache: gradle - - name: Copy new api.txt files + - name: Copy existing api.txt files run: ./gradlew copyApiTxtFile - - name: Checkout main + - name: Checkout PR uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ github.base_ref }} + ref: ${{ github.head_ref || github.ref_name }} clean: false - name: Run Metalava SemVer check diff --git a/.github/workflows/plugins-check.yml b/.github/workflows/plugins-check.yml index 3cbb6d2d01b..f33b1724a41 100644 --- a/.github/workflows/plugins-check.yml +++ b/.github/workflows/plugins-check.yml @@ -30,6 +30,7 @@ jobs: ./gradlew plugins:check - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 + if: always() with: files: "**/build/test-results/**/*.xml" check_name: "plugins test results" diff --git a/.github/workflows/post_release_cleanup.yml b/.github/workflows/post_release_cleanup.yml index d7ee562bb51..8290b977148 100644 --- a/.github/workflows/post_release_cleanup.yml +++ b/.github/workflows/post_release_cleanup.yml @@ -24,6 +24,7 @@ jobs: - name: Run post release cleanup task run: | ./gradlew postReleaseCleanup + ./gradlew spotlessApply - name: Create Pull Request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 diff --git a/.github/workflows/sessions-e2e.yml b/.github/workflows/sessions-e2e.yml index 092a51fc094..6fafc4cc9e2 100644 --- a/.github/workflows/sessions-e2e.yml +++ b/.github/workflows/sessions-e2e.yml @@ -23,7 +23,7 @@ jobs: - name: set up JDK 17 uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: - java-version: '11' + java-version: '17' distribution: 'temurin' cache: gradle @@ -39,4 +39,4 @@ jobs: env: FTL_RESULTS_BUCKET: fireescape run: | - ./gradlew :firebase-sessions:test-app:deviceCheck withErrorProne -PtargetBackend="prod" -PtriggerCrashes + ./gradlew :firebase-sessions:test-app:deviceCheck withErrorProne -PtargetBackend="prod" diff --git a/.github/workflows/update-cpp-sdk-on-release.yml b/.github/workflows/update-cpp-sdk-on-release.yml index 49e6b0e1392..57e1e143ee3 100644 --- a/.github/workflows/update-cpp-sdk-on-release.yml +++ b/.github/workflows/update-cpp-sdk-on-release.yml @@ -53,7 +53,7 @@ jobs: - name: Setup python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - python-version: 3.7 + python-version: 3.9 - name: Check out firebase-cpp-sdk uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0360670700..8dbfd45addb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,24 @@ # How to Contribute -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. +We'd love to accept your patches and contributions to this project. There are just a few small +guidelines you need to follow. ## Contributor License Agreement -Contributions to this project must be accompanied by a Contributor License -Agreement. You (or your employer) retain the copyright to your contribution; -this simply gives us permission to use and redistribute your contributions as -part of the project. Head over to to see -your current agreements on file or to sign a new one. +Contributions to this project must be accompanied by a Contributor License Agreement. You (or your +employer) retain the copyright to your contribution; this simply gives us permission to use and +redistribute your contributions as part of the project. Head over to + to see your current agreements on file or to sign a new one. -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. +You generally only need to submit a CLA once, so if you've already submitted one (even if it was for +a different project), you probably don't need to do it again. ## Code reviews -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. +All submissions, including submissions by project members, require review. We use GitHub pull +requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using +pull requests. ## Community Guidelines @@ -29,5 +27,6 @@ This project follows ## Contributor Documentation -To know more about how to setup your environment, how Firebase internals work, and -best practices, take a look at our detailed [contributor documentation](https://firebase.github.io/firebase-android-sdk/). \ No newline at end of file +To know more about how to setup your environment, how Firebase internals work, and best practices, +take a look at our detailed +[contributor documentation](https://firebase.github.io/firebase-android-sdk/). diff --git a/README.md b/README.md index a1cb29151c5..948d6254ce3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # Firebase Android Open Source Development -This repository contains the source code for all Android Firebase SDKs except -Analytics and Auth. +This repository contains the source code for all Android Firebase SDKs except Analytics and Auth. -Firebase is an app development platform with tools to help you build, grow and -monetize your app. More information about Firebase can be found at -https://firebase.google.com. +Firebase is an app development platform with tools to help you build, grow and monetize your app. +More information about Firebase can be found at https://firebase.google.com. ## Table of contents @@ -15,10 +13,8 @@ https://firebase.google.com. 1. [Integration Testing](#integration-testing) 1. [Proguarding](#proguarding) 1. [APIs used via reflection](#APIs-used-via-reflection) - 1. [APIs intended for developer - consumption](#APIs-intended-for-developer-consumption) - 1. [APIs intended for other Firebase - SDKs](#APIs-intended-for-other-firebase-sdks) + 1. [APIs intended for developer consumption](#APIs-intended-for-developer-consumption) + 1. [APIs intended for other Firebase SDKs](#APIs-intended-for-other-firebase-sdks) 1. [Publishing](#publishing) 1. [Dependencies](#dependencies) 1. [Commands](#commands) @@ -27,23 +23,22 @@ https://firebase.google.com. ## Getting Started -* Install the latest Android Studio (should be 3.0.1 or later) -* Clone the repo (`git clone --recurse-submodules git@github.com:firebase/firebase-android-sdk.git`) - * When cloning the repo, it is important to get the submodules as well. If - you have already cloned the repo without the submodules, you can update the - submodules by running `git submodule update --init --recursive`. -* Import the firebase-android-sdk gradle project into Android Studio using the - **Import project(Gradle, Eclipse ADT, etc.)** option. -* `firebase-crashlytics-ndk` must be built with NDK 21. See - [firebase-crashlytics-ndk](firebase-crashlytics-ndk/README.md) for more - details. +- Install the latest Android Studio (should be Meerkat | 2024.3.1 or later) +- Clone the repo (`git clone --recurse-submodules git@github.com:firebase/firebase-android-sdk.git`) + - When cloning the repo, it is important to get the submodules as well. If you have already cloned + the repo without the submodules, you can update the submodules by running + `git submodule update --init --recursive`. +- Import the firebase-android-sdk gradle project into Android Studio using the **Import + project(Gradle, Eclipse ADT, etc.)** option. +- `firebase-crashlytics-ndk` must be built with NDK 21. See + [firebase-crashlytics-ndk](firebase-crashlytics-ndk/README.md) for more details. ## Testing Firebase Android libraries exercise all three types of tests recommended by the [Android Testing Pyramid](https://developer.android.com/training/testing/fundamentals#testing-pyramid). -Depending on the requirements of the specific project, some or all of these -tests may be used to support changes. +Depending on the requirements of the specific project, some or all of these tests may be used to +support changes. > :warning: **Running tests with errorprone** > @@ -53,53 +48,46 @@ tests may be used to support changes. ### Unit Testing -These are tests that run on your machine's local Java Virtual Machine (JVM). At -runtime, these tests are executed against a modified version of android.jar -where all final modifiers have been stripped off. This lets us sandbox behaviors -at desired places and use popular mocking libraries. +These are tests that run on your machine's local Java Virtual Machine (JVM). At runtime, these tests +are executed against a modified version of android.jar where all final modifiers have been stripped +off. This lets us sandbox behaviors at desired places and use popular mocking libraries. Unit tests can be executed on the command line by running + ```bash ./gradlew ::check ``` -#### Vertex AI for Firebase - -See the Vertex AI for Firebase [README](firebase-vertexai#running-tests) for setup -instructions specific to that project. - ### Integration Testing -These are tests that run on a hardware device or emulator. These tests have -access to Instrumentation APIs, give you access to information such as the -[Android Context](https://developer.android.com/reference/android/content/Context). -In Firebase, instrumentation tests are used at different capacities by different -projects. Some tests may exercise device capabilities, while stubbing any calls -to the backend, while some others may call out to nightly backend builds to -ensure distributed API compatibility. +These are tests that run on a hardware device or emulator. These tests have access to +Instrumentation APIs, give you access to information such as the +[Android Context](https://developer.android.com/reference/android/content/Context). In Firebase, +instrumentation tests are used at different capacities by different projects. Some tests may +exercise device capabilities, while stubbing any calls to the backend, while some others may call +out to nightly backend builds to ensure distributed API compatibility. -Along with Espresso, they are also used to test projects that have UI -components. +Along with Espresso, they are also used to test projects that have UI components. #### Project Setup -Before you can run integration tests, you need to add a `google-services.json` -file to the root of your checkout. You can use the `google-services.json` from -any project that includes an Android App, though you'll likely want one that's -separate from any production data you have because our tests write random data. +Before you can run integration tests, you need to add a `google-services.json` file to the root of +your checkout. You can use the `google-services.json` from any project that includes an Android App, +though you'll likely want one that's separate from any production data you have because our tests +write random data. If you don't have a suitable testing project already: - * Open the [Firebase console](https://console.firebase.google.com/) - * If you don't yet have a project you want to use for testing, create one. - * Add an Android app to the project - * Give the app any package name you like. - * Download the resulting `google-services.json` file and put it in the root of - your checkout. +- Open the [Firebase console](https://console.firebase.google.com/) +- If you don't yet have a project you want to use for testing, create one. +- Add an Android app to the project +- Give the app any package name you like. +- Download the resulting `google-services.json` file and put it in the root of your checkout. #### Running Integration Tests on Local Emulator Integration tests can be executed on the command line by running + ```bash ./gradlew ::connectedCheck ``` @@ -108,11 +96,12 @@ Integration tests can be executed on the command line by running > You need additional setup for this to work: > -> * `gcloud` needs to be [installed](https://cloud.google.com/sdk/install) on local machine -> * `gcloud` needs to be configured with a project that has billing enabled -> * `gcloud` needs to be authenticated with credentials that have 'Firebase Test Lab Admin' role +> - `gcloud` needs to be [installed](https://cloud.google.com/sdk/install) on local machine +> - `gcloud` needs to be configured with a project that has billing enabled +> - `gcloud` needs to be authenticated with credentials that have 'Firebase Test Lab Admin' role Integration tests can be executed on the command line by running + ```bash ./gradlew ::deviceCheck ``` @@ -139,70 +128,62 @@ Firebase SDKs use some special annotations for tooling purposes. ### @Keep APIs that need to be preserved up until the app's runtime can be annotated with -[@Keep](https://developer.android.com/reference/android/support/annotation/Keep). -The -[@Keep](https://developer.android.com/reference/android/support/annotation/Keep) -annotation is *blessed* to be honored by android's [default proguard -configuration](https://developer.android.com/studio/write/annotations#keep). A common use for -this annotation is because of reflection. These APIs should be generally **discouraged**, because -they can't be proguarded. +[@Keep](https://developer.android.com/reference/android/support/annotation/Keep). The +[@Keep](https://developer.android.com/reference/android/support/annotation/Keep) annotation is +_blessed_ to be honored by android's +[default proguard configuration](https://developer.android.com/studio/write/annotations#keep). A +common use for this annotation is because of reflection. These APIs should be generally +**discouraged**, because they can't be proguarded. ### @KeepForSdk -APIs that are intended to be used by Firebase SDKs should be annotated with -`@KeepForSdk`. The key benefit here is that the annotation is *blessed* to throw -linter errors on Android Studio if used by the developer from a non firebase -package, thereby providing a valuable guard rail. - +APIs that are intended to be used by Firebase SDKs should be annotated with `@KeepForSdk`. The key +benefit here is that the annotation is _blessed_ to throw linter errors on Android Studio if used by +the developer from a non firebase package, thereby providing a valuable guard rail. ### @PublicApi We annotate APIs that meant to be used by developers with -[@PublicAPI](firebase-common/src/main/java/com/google/firebase/annotations/PublicApi.java). This +[@PublicAPI](firebase-common/src/main/java/com/google/firebase/annotations/PublicApi.java). This annotation will be used by tooling to help inform the version bump (major, minor, patch) that is required for the next release. ## Proguarding -Firebase SDKs do not proguard themselves, but support proguarding. Firebase SDKs themselves are +Firebase SDKs do not proguard themselves, but support proguarding. Firebase SDKs themselves are proguard friendly, but the dependencies of Firebase SDKs may not be. ### Proguard config -In addition to preguard.txt, projects declare an additional set of proguard -rules in a proguard.txt that are honored by the developer's app while building -the app's proguarded apk. This file typically contains the keep rules that need -to be honored during the app' s proguarding phase. +In addition to preguard.txt, projects declare an additional set of proguard rules in a proguard.txt +that are honored by the developer's app while building the app's proguarded apk. This file typically +contains the keep rules that need to be honored during the app' s proguarding phase. -As a best practice, these explicit rules should be scoped to only libraries -whose source code is outside the firebase-android-sdk codebase making annotation -based approaches insufficient.The combination of keep rules resulting from the -annotations, the preguard.txt and the proguard.txt collectively determine the -APIs that are preserved at **runtime**. +As a best practice, these explicit rules should be scoped to only libraries whose source code is +outside the firebase-android-sdk codebase making annotation based approaches insufficient.The +combination of keep rules resulting from the annotations, the preguard.txt and the proguard.txt +collectively determine the APIs that are preserved at **runtime**. ## Publishing -Firebase is published as a collection of libraries each of which either -represents a top level product, or contains shared functionality used by one or -more projects. The projects are published as managed maven artifacts available -at [Google's Maven Repository](https://maven.google.com). This section helps -reason about how developers may make changes to firebase projects and have their -apps depend on the modified versions of Firebase. +Firebase is published as a collection of libraries each of which either represents a top level +product, or contains shared functionality used by one or more projects. The projects are published +as managed maven artifacts available at [Google's Maven Repository](https://maven.google.com). This +section helps reason about how developers may make changes to firebase projects and have their apps +depend on the modified versions of Firebase. ### Dependencies Any dependencies, within the projects, or outside of Firebase are encoded as [maven dependencies](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) -into the `pom` file that accompanies the published artifact. This allows the -developer's build system (typically Gradle) to build a dependency graph and -select the dependencies using its own [resolution -strategy](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html) +into the `pom` file that accompanies the published artifact. This allows the developer's build +system (typically Gradle) to build a dependency graph and select the dependencies using its own +[resolution strategy](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html) ### Commands -For more advanced use cases where developers wish to make changes to a project, -but have transitive dependencies point to publicly released versions, individual -projects may be published as follows. +For more advanced use cases where developers wish to make changes to a project, but have transitive +dependencies point to publicly released versions, individual projects may be published as follows. ```bash # e.g. to publish Firestore and Functions @@ -210,21 +191,21 @@ projects may be published as follows. publishReleasingLibrariesToMavenLocal ``` -Developers may take a dependency on these locally published versions by adding -the `mavenLocal()` repository to your [repositories -block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in -your app module's build.gradle. +Developers may take a dependency on these locally published versions by adding the `mavenLocal()` +repository to your +[repositories block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in your +app module's build.gradle. ### Code Formatting Java and Kotlin are both formatted using `spotless`. To run formatting on a project, run + ```bash ./gradlew ::spotlessApply ``` ### Contributing -We love contributions! Please read our -[contribution guidelines](/CONTRIBUTING.md) to get started. +We love contributions! Please read our [contribution guidelines](/CONTRIBUTING.md) to get started. diff --git a/agents.md b/agents.md new file mode 100644 index 00000000000..bd058e68114 --- /dev/null +++ b/agents.md @@ -0,0 +1,197 @@ +# Agents + +This guide provides essential information for working within the `firebase-android-sdk` repository. + +## Project Overview + +This repository contains the source code for the Firebase Android SDKs. It is a large, multi-module +Gradle project. The project is written in a mix of Java and Kotlin. + +The project is structured as a collection of libraries, each representing a Firebase product or a +shared component. These libraries are published as Maven artifacts to Google's Maven Repository. + +## Project Structure + +The `subprojects.cfg` file lists all the subprojects in this repository. Each line in this file +follows the format ` # `, where `project-type` can be one of the +following: + +- `sdk`: A public-facing SDK that is published. +- `test`: A test application or a test-only module. +- `util`: A utility module that is not part of the public API. +- `directory`: A directory containing other subprojects. + +This file is useful for understanding the role of each subproject in the repository. + +## Environment Setup + +To work with this repository, the Android SDK must be installed. Use the `sdkmanager` command-line +tool for this purpose. + +1. **Install Android SDK Command-Line Tools**: + + - If not already installed, download the command-line tools from the + [Android Studio page](https://developer.android.com/studio#command-line-tools-only). + - Create a directory for the Android SDK, e.g., `android_sdk`. + - Unzip the downloaded package. This will create a `cmdline-tools` directory. Move this + directory to `android_sdk/cmdline-tools/latest`. + - The final structure should be `android_sdk/cmdline-tools/latest/`. + +2. **Install required SDK packages**: + + - Use `sdkmanager` to install the necessary platforms, build tools, and other packages. For + example: + + ```bash + # List all available packages + sdkmanager --list + + # Install platform tools and the SDK for API level 33 + sdkmanager "platform-tools" "platforms;android-33" + + # Accept all licenses + yes | sdkmanager --licenses + ``` + + - Refer to the specific requirements of the project to determine which packages to install. + +3. **Configure for integration tests**: + + - To run integration tests, a `google-services.json` file is required. + - Place this file in the root of the repository. + +4. **Install NDK for specific projects**: + - Some projects, like `firebase-crashlytics-ndk`, require a specific version of the Android NDK. + You can install it using `sdkmanager`. For example, to install NDK version 21.4.7075529, you + would run `sdkmanager "ndk;21.4.7075529"`. Always refer to the project's `README.md` for the + exact version required. + +## Building and Running + +The project is built using Gradle. The `gradlew` script is provided in the root directory. + +### Building + +To build the entire project, you can run the following command: + +```bash +./gradlew build +``` + +To build a specific project, you can run: + +```bash +./gradlew ::build +``` + +### Running Tests + +The project has three types of tests: unit tests, integration tests, and smoke tests. + +#### Unit Tests + +Unit tests run on the local JVM. They can be executed with the following command: + +```bash +./gradlew ::check +``` + +#### Integration Tests + +Integration tests run on a hardware device or emulator. Before running integration tests, you need +to add a `google-services.json` file to the root of the project. + +To run integration tests on a local emulator, use the following command: + +```bash +./gradlew ::connectedCheck +``` + +To run integration tests on Firebase Test Lab, use the following command: + +```bash +./gradlew ::deviceCheck +``` + +### Publishing + +To publish a project locally, you can use the following command: + +```bash +./gradlew -PprojectsToPublish="" publishReleasingLibrariesToMavenLocal +``` + +## Development Conventions + +### Code Formatting + +The project uses Spotless for code formatting. To format the code, run the following command: + +```bash +./gradlew spotlessApply +``` + +To format a specific project, run: + +```bash +./gradlew ::spotlessApply +``` + +### API Surface + +The public API of the Firebase SDKs is managed using a set of annotations: + +- `@PublicApi`: Marks APIs that are intended for public consumption by developers. +- `@KeepForSdk`: Marks APIs that are intended for use by other Firebase SDKs. These APIs will + trigger a linter error if used by developers outside of a Firebase package. +- `@Keep`: Marks APIs that need to be preserved at runtime, usually due to reflection. This + annotation should be used sparingly as it prevents Proguard from removing or renaming the code. + +### Common Patterns + +This repository uses a combination of dependency injection frameworks: + +- **`firebase-components`**: This is a custom dependency injection framework used for discovery and + dependency injection between different Firebase SDKs. It allows SDKs to register their components + and declare dependencies on other components. The initialization is managed by `FirebaseApp`. + +- **Dagger**: Dagger is used for internal dependency injection within individual SDKs. This helps to + create more testable and maintainable code. Dagger components are typically instantiated within + the `ComponentRegistrar` of an SDK, which allows for the injection of dependencies from + `firebase-components` into the Dagger graph. + +### Proguarding + +The project supports Proguarding. Proguard rules are defined in `proguard.txt` files within each +project. + +## External Dependencies + +Do not add, under any circunstance, any new dependency to a SDK that does not already exists in the +`gradle/libs.versions.toml`, and even then, only do it if cxexplicitly asked to do so. The Firebase +SDKs are designed to be lightweight, and adding new dependencies can increase the size of the final +artifacts. + +## Contributing + +Contributions are welcome. Please read the [contribution guidelines](/CONTRIBUTING.md) to get +started. + +## Iteration Loop + +After you make a change, here's the flow you should follow: + +- Format the code using `spotless`. It can be run with: + ```bash + ./gradlew ::spotlessApply + ``` +- Run unit tests: + ```bash + ./gradlew ::check + ``` +- If necessary, run integration tests based on the instructions above. + +## Updating this Guide + +If new patterns or conventions are discovered, update this guide to ensure it remains a useful +resource. diff --git a/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md index afaeae8a097..d3ff3c5361b 100644 --- a/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md +++ b/appcheck/firebase-appcheck-debug-testing/CHANGELOG.md @@ -1,64 +1,82 @@ # Unreleased +# 19.0.1 + +- [changed] Bumped internal dependencies. + +# 19.0.0 + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 18.0.0 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 17.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.1.1 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.1.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.0.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.1 -* [changed] Integrated the [app_check] Debug Testing SDK with Firebase - components. - (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) + +- [changed] Integrated the [app_check] Debug Testing SDK with Firebase components. (GitHub + [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) # 16.1.0 -* [unchanged] Updated to accommodate the release of the updated - [app_check] Kotlin extensions library. + +- [unchanged] Updated to accommodate the release of the updated [app_check] Kotlin extensions + library. # 16.0.1 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). + +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). # 16.0.0 -* [changed] [app_check] has exited beta and is now generally available for - use. + +- [changed] [app_check] has exited beta and is now generally available for use. # 16.0.0-beta06 -* [fixed] Fixed a bug in the [app_check] token refresh flow when using a - custom provider. + +- [fixed] Fixed a bug in the [app_check] token refresh flow when using a custom provider. # 16.0.0-beta05 -* [changed] Internal improvements. + +- [changed] Internal improvements. # 16.0.0-beta04 -* [changed] Improved error handling logic by minimizing the amount of requests - that are unlikely to succeed. -* [fixed] Fixed heartbeat reporting. + +- [changed] Improved error handling logic by minimizing the amount of requests that are unlikely to + succeed. +- [fixed] Fixed heartbeat reporting. # 16.0.0-beta03 -* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to - [app_check] network calls. + +- [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to [app_check] network + calls. # 16.0.0-beta02 -* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + +- [feature] Added + [`getAppCheckToken()`](), [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), - and associated setters and removers for developers to request and observe - changes to the [app_check] token. + and associated setters and removers for developers to request and observe changes to the + [app_check] token. # 16.0.0-beta01 -* [feature] Initial beta release of the [app_check] Debug Testing SDK with - abuse reduction features. +- [feature] Initial beta release of the [app_check] Debug Testing SDK with abuse reduction features. 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 03624b20da0..1f09923da89 100644 --- a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle +++ b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle @@ -14,7 +14,7 @@ plugins { id 'firebase-library' - id 'copy-google-services' +// id 'copy-google-services' } firebaseLibrary { @@ -23,7 +23,6 @@ firebaseLibrary { releaseNotes { name.set("{{app_check}} Debug Testing") versionName.set("appcheck-debug-testing") - hasKTX.set(false) } } @@ -55,8 +54,8 @@ dependencies { api project(':appcheck:firebase-appcheck') api project(':appcheck:firebase-appcheck-debug') api 'com.google.firebase:firebase-appcheck-interop:17.0.0' - api 'com.google.firebase:firebase-common:21.0.0' - api 'com.google.firebase:firebase-components:18.0.0' + api libs.firebase.common + api libs.firebase.components implementation libs.androidx.test.core implementation libs.playservices.base diff --git a/appcheck/firebase-appcheck-debug-testing/gradle.properties b/appcheck/firebase-appcheck-debug-testing/gradle.properties index 9b7be4891d1..04d7bc444aa 100644 --- a/appcheck/firebase-appcheck-debug-testing/gradle.properties +++ b/appcheck/firebase-appcheck-debug-testing/gradle.properties @@ -1,2 +1,2 @@ -version=18.0.1 -latestReleasedVersion=18.0.0 +version=19.0.2 +latestReleasedVersion=19.0.1 diff --git a/appcheck/firebase-appcheck-debug/CHANGELOG.md b/appcheck/firebase-appcheck-debug/CHANGELOG.md index ced2dde64d0..b0e1b2b44ec 100644 --- a/appcheck/firebase-appcheck-debug/CHANGELOG.md +++ b/appcheck/firebase-appcheck-debug/CHANGELOG.md @@ -1,69 +1,87 @@ # Unreleased +# 19.0.1 + +- [changed] Bumped internal dependencies. + +# 19.0.0 + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 18.0.0 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 17.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.1.1 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.1.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.0.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.1 -* [changed] Migrated [app_check] SDKs to use standard Firebase executors. - (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} - and + +- [changed] Migrated [app_check] SDKs to use standard Firebase executors. (GitHub + [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} and [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) -* [changed] Integrated the [app_check] Debug SDK with Firebase components. - (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) -* [changed] Moved Task continuations off the main thread. - (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) +- [changed] Integrated the [app_check] Debug SDK with Firebase components. (GitHub + [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) +- [changed] Moved Task continuations off the main thread. (GitHub + [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) # 16.1.0 -* [unchanged] Updated to accommodate the release of the updated - [app_check] Kotlin extensions library. + +- [unchanged] Updated to accommodate the release of the updated [app_check] Kotlin extensions + library. # 16.0.1 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). + +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). # 16.0.0 -* [changed] [app_check] has exited beta and is now generally available for - use. + +- [changed] [app_check] has exited beta and is now generally available for use. # 16.0.0-beta06 -* [fixed] Fixed a bug in the [app_check] token refresh flow when using a - custom provider. + +- [fixed] Fixed a bug in the [app_check] token refresh flow when using a custom provider. # 16.0.0-beta05 -* [changed] Internal improvements. + +- [changed] Internal improvements. # 16.0.0-beta04 -* [changed] Improved error handling logic by minimizing the amount of requests - that are unlikely to succeed. -* [fixed] Fixed heartbeat reporting. + +- [changed] Improved error handling logic by minimizing the amount of requests that are unlikely to + succeed. +- [fixed] Fixed heartbeat reporting. # 16.0.0-beta03 -* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to - [app_check] network calls. + +- [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to [app_check] network + calls. # 16.0.0-beta02 -* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + +- [feature] Added + [`getAppCheckToken()`](), [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), - and associated setters and removers for developers to request and observe - changes to the [app_check] token. + and associated setters and removers for developers to request and observe changes to the + [app_check] token. # 16.0.0-beta01 -* [feature] Initial beta release of the [app_check] Debug SDK with abuse - reduction features. +- [feature] Initial beta release of the [app_check] Debug SDK with abuse reduction features. diff --git a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle index 44a14355d03..ad1dddda818 100644 --- a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle +++ b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle @@ -21,7 +21,6 @@ firebaseLibrary { releaseNotes { name.set("{{app_check}} Debug") versionName.set("appcheck-debug") - hasKTX.set(false) } } @@ -50,9 +49,9 @@ dependencies { javadocClasspath libs.autovalue.annotations api project(':appcheck:firebase-appcheck') - api 'com.google.firebase:firebase-annotations:16.2.0' - api 'com.google.firebase:firebase-common:21.0.0' - api 'com.google.firebase:firebase-components:18.0.0' + api libs.firebase.annotations + api libs.firebase.common + api libs.firebase.components implementation platform(libs.kotlin.bom) implementation libs.playservices.base diff --git a/appcheck/firebase-appcheck-debug/gradle.properties b/appcheck/firebase-appcheck-debug/gradle.properties index 9b7be4891d1..04d7bc444aa 100644 --- a/appcheck/firebase-appcheck-debug/gradle.properties +++ b/appcheck/firebase-appcheck-debug/gradle.properties @@ -1,2 +1,2 @@ -version=18.0.1 -latestReleasedVersion=18.0.0 +version=19.0.2 +latestReleasedVersion=19.0.1 diff --git a/appcheck/firebase-appcheck-interop/CHANGELOG.md b/appcheck/firebase-appcheck-interop/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/appcheck/firebase-appcheck-interop/CHANGELOG.md +++ b/appcheck/firebase-appcheck-interop/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md index 391a2d955f3..5a7b56788c0 100644 --- a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md +++ b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md @@ -1,46 +1,58 @@ # Unreleased +# 19.0.1 + +- [changed] Bumped internal dependencies. + +# 19.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. # 18.0.0 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 17.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.1.1 -* [fixed] Fixed client-side throttling in Play Integrity flows. -* [changed] Bumped Play Integrity API Library dependency version. + +- [fixed] Fixed client-side throttling in Play Integrity flows. +- [changed] Bumped Play Integrity API Library dependency version. # 17.1.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.0.0 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. + +- [unchanged] Updated to keep [app_check] SDK versions aligned. # 16.1.1 -* [changed] Migrated [app_check] SDKs to use standard Firebase executors. - (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} - and + +- [changed] Migrated [app_check] SDKs to use standard Firebase executors. (GitHub + [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} and [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) -* [changed] Integrated the [app_check] Play integrity SDK with Firebase - components. - (GitHub [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) -* [changed] Moved Task continuations off the main thread. - (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) +- [changed] Integrated the [app_check] Play integrity SDK with Firebase components. (GitHub + [#4436](//github.com/firebase/firebase-android-sdk/issues/4436){: .external}) +- [changed] Moved Task continuations off the main thread. (GitHub + [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) # 16.1.0 -* [unchanged] Updated to accommodate the release of the updated - [app_check] Kotlin extensions library. + +- [unchanged] Updated to accommodate the release of the updated [app_check] Kotlin extensions + library. # 16.0.1 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). + +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). # 16.0.0 -* [feature] Added support for - [Play Integrity](https://developer.android.com/google/play/integrity) as an - attestation provider. +- [feature] Added support for [Play Integrity](https://developer.android.com/google/play/integrity) + as an attestation provider. diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle index 975401af098..4a8cbbb9dfb 100644 --- a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -21,7 +21,6 @@ firebaseLibrary { releaseNotes { name.set("{{app_check}} Play integrity") versionName.set("appcheck-playintegrity") - hasKTX.set(false) } } @@ -50,10 +49,9 @@ dependencies { javadocClasspath libs.autovalue.annotations api project(':appcheck:firebase-appcheck') - api 'com.google.firebase:firebase-annotations:16.2.0' - api 'com.google.firebase:firebase-common:21.0.0' - api 'com.google.firebase:firebase-common-ktx:21.0.0' - api 'com.google.firebase:firebase-components:18.0.0' + api libs.firebase.annotations + api libs.firebase.common + api libs.firebase.components implementation libs.playservices.base implementation libs.playservices.tasks diff --git a/appcheck/firebase-appcheck-playintegrity/gradle.properties b/appcheck/firebase-appcheck-playintegrity/gradle.properties index 9b7be4891d1..04d7bc444aa 100644 --- a/appcheck/firebase-appcheck-playintegrity/gradle.properties +++ b/appcheck/firebase-appcheck-playintegrity/gradle.properties @@ -1,2 +1,2 @@ -version=18.0.1 -latestReleasedVersion=18.0.0 +version=19.0.2 +latestReleasedVersion=19.0.1 diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/api.txt b/appcheck/firebase-appcheck-recaptchaenterprise/api.txt new file mode 100644 index 00000000000..99d21ff038c --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/api.txt @@ -0,0 +1,10 @@ +// Signature format: 3.0 +package com.google.firebase.appcheck.recaptchaenterprise { + + public class RecaptchaEnterpriseAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory { + method public com.google.firebase.appcheck.AppCheckProvider create(com.google.firebase.FirebaseApp); + method public static com.google.firebase.appcheck.recaptchaenterprise.RecaptchaEnterpriseAppCheckProviderFactory getInstance(String); + } + +} + diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/existing_api.txt b/appcheck/firebase-appcheck-recaptchaenterprise/existing_api.txt new file mode 100644 index 00000000000..99d21ff038c --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/existing_api.txt @@ -0,0 +1,10 @@ +// Signature format: 3.0 +package com.google.firebase.appcheck.recaptchaenterprise { + + public class RecaptchaEnterpriseAppCheckProviderFactory implements com.google.firebase.appcheck.AppCheckProviderFactory { + method public com.google.firebase.appcheck.AppCheckProvider create(com.google.firebase.FirebaseApp); + method public static com.google.firebase.appcheck.recaptchaenterprise.RecaptchaEnterpriseAppCheckProviderFactory getInstance(String); + } + +} + diff --git a/appcheck/firebase-appcheck/ktx/ktx.gradle b/appcheck/firebase-appcheck-recaptchaenterprise/firebase-appcheck-recaptchaenterprise.gradle similarity index 53% rename from appcheck/firebase-appcheck/ktx/ktx.gradle rename to appcheck/firebase-appcheck-recaptchaenterprise/firebase-appcheck-recaptchaenterprise.gradle index ed498a84e28..045736b6f4f 100644 --- a/appcheck/firebase-appcheck/ktx/ktx.gradle +++ b/appcheck/firebase-appcheck-recaptchaenterprise/firebase-appcheck-recaptchaenterprise.gradle @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,60 +14,56 @@ plugins { id 'firebase-library' - id("kotlin-android") } firebaseLibrary { - libraryGroup = "appcheck" - testLab.enabled = true - publishJavadoc = false - releaseNotes { - enabled.set(false) + libraryGroup = "appcheck-norelease" + releaseNotes { + name.set("{{app_check}} Recaptcha Enterprise") + versionName.set("appcheck-recaptchaenterprise") } + previewMode = "norelease" } android { - namespace "com.google.firebase.appcheck.ktx" + adbOptions { + timeOutInMs 60 * 1000 + } + + namespace "com.google.firebase.appcheck.recaptchaenterprise" compileSdkVersion project.compileSdkVersion defaultConfig { - minSdkVersion project.minSdkVersion - multiDexEnabled true targetSdkVersion project.targetSdkVersion + minSdkVersion project.minSdkVersion 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' - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } - testOptions.unitTests.includeAndroidResources = true + + testOptions.unitTests.includeAndroidResources = false } dependencies { - api(project(":appcheck:firebase-appcheck")) - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-common-ktx:21.0.0") + implementation(libs.dagger.dagger) + + api project(':appcheck:firebase-appcheck') + api libs.firebase.common + api libs.firebase.components + api 'com.google.android.recaptcha:recaptcha:18.7.1' - implementation("com.google.firebase:firebase-components:18.0.0") + annotationProcessor(libs.dagger.compiler) + testImplementation(project(":integ-testing")) { + exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-components' + } + testImplementation libs.androidx.test.core testImplementation libs.truth testImplementation libs.junit testImplementation libs.mockito.core testImplementation libs.robolectric - - androidTestImplementation libs.androidx.test.core - androidTestImplementation libs.androidx.test.runner - androidTestImplementation 'com.google.firebase:firebase-appcheck-interop:17.1.0' - androidTestImplementation libs.truth - androidTestImplementation libs.junit -} + testImplementation libs.org.json +} \ No newline at end of file diff --git a/firebase-ml-modeldownloader/ktx/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/AndroidManifest.xml similarity index 78% rename from firebase-ml-modeldownloader/ktx/src/main/AndroidManifest.xml rename to appcheck/firebase-appcheck-recaptchaenterprise/src/main/AndroidManifest.xml index 105842d8ac4..7d331ed90c3 100644 --- a/firebase-ml-modeldownloader/ktx/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - + @@ -12,13 +11,16 @@ + - - + + + - \ No newline at end of file + diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrar.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrar.java new file mode 100644 index 00000000000..2a87456ed69 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrar.java @@ -0,0 +1,64 @@ +// Copyright 2025 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.recaptchaenterprise; + +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.recaptchaenterprise.internal.DaggerProviderComponent; +import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent; +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 reCAPTCHA Enterprise's dependency + * injections in Firebase Android Components. + * + * @hide + */ +@KeepForSdk +public class FirebaseAppCheckRecaptchaEnterpriseRegistrar implements ComponentRegistrar { + private static final String LIBRARY_NAME = "fire-app-check-recaptcha-enterprise"; + + @Override + public List> getComponents() { + Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); + Qualified blockingExecutor = Qualified.qualified(Blocking.class, Executor.class); + + return Arrays.asList( + Component.builder(ProviderMultiResourceComponent.class) + .name(LIBRARY_NAME) + .add(Dependency.required(FirebaseApp.class)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(blockingExecutor)) + .factory( + container -> + DaggerProviderComponent.builder() + .setFirebaseApp(container.get(FirebaseApp.class)) + .setLiteExecutor(container.get(liteExecutor)) + .setBlockingExecutor(container.get(blockingExecutor)) + .build() + .getMultiResourceComponent()) + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactory.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactory.java new file mode 100644 index 00000000000..e051f54d0e9 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactory.java @@ -0,0 +1,62 @@ +// Copyright 2025 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.recaptchaenterprise; + +import androidx.annotation.NonNull; +import com.google.firebase.FirebaseApp; +import com.google.firebase.appcheck.AppCheckProvider; +import com.google.firebase.appcheck.AppCheckProviderFactory; +import com.google.firebase.appcheck.FirebaseAppCheck; +import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent; +import com.google.firebase.appcheck.recaptchaenterprise.internal.RecaptchaEnterpriseAppCheckProvider; +import java.util.Objects; + +/** + * Implementation of an {@link AppCheckProviderFactory} that builds
+ * {@link RecaptchaEnterpriseAppCheckProvider}s. This is the default implementation. + */ +public class RecaptchaEnterpriseAppCheckProviderFactory implements AppCheckProviderFactory { + + private final String siteKey; + private volatile RecaptchaEnterpriseAppCheckProvider provider; + + private RecaptchaEnterpriseAppCheckProviderFactory(@NonNull String siteKey) { + this.siteKey = siteKey; + } + + /** Gets an instance of this class for installation into a {@link FirebaseAppCheck} instance. */ + @NonNull + public static RecaptchaEnterpriseAppCheckProviderFactory getInstance(@NonNull String siteKey) { + Objects.requireNonNull(siteKey, "siteKey cannot be null"); + return new RecaptchaEnterpriseAppCheckProviderFactory(siteKey); + } + + @NonNull + @Override + @SuppressWarnings("FirebaseUseExplicitDependencies") + public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) { + if (provider == null) { + synchronized (this) { + if (provider == null) { + ProviderMultiResourceComponent component = + firebaseApp.get(ProviderMultiResourceComponent.class); + provider = component.get(siteKey); + provider.initializeRecaptchaClient(); + } + } + } + return provider; + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequest.java new file mode 100644 index 00000000000..76107a7ad0e --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequest.java @@ -0,0 +1,44 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Client-side model of the ExchangeRecaptchaEnterpriseTokenRequest payload from the Firebase App + * Check Token Exchange API. + */ +public class ExchangeRecaptchaEnterpriseTokenRequest { + + @VisibleForTesting + static final String RECAPTCHA_ENTERPRISE_TOKEN_KEY = "recaptchaEnterpriseToken"; + + private final String recaptchaEnterpriseToken; + + public ExchangeRecaptchaEnterpriseTokenRequest(@NonNull String recaptchaEnterpriseToken) { + this.recaptchaEnterpriseToken = recaptchaEnterpriseToken; + } + + @NonNull + public String toJsonString() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(RECAPTCHA_ENTERPRISE_TOKEN_KEY, recaptchaEnterpriseToken); + + return jsonObject.toString(); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderComponent.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderComponent.java new file mode 100644 index 00000000000..356a646bd5b --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderComponent.java @@ -0,0 +1,47 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Blocking; +import com.google.firebase.annotations.concurrent.Lightweight; +import dagger.BindsInstance; +import dagger.Component; +import dagger.Module; +import java.util.concurrent.Executor; +import javax.inject.Singleton; + +@Singleton +@Component(modules = ProviderComponent.MainModule.class) +public interface ProviderComponent { + ProviderMultiResourceComponent getMultiResourceComponent(); + + @Component.Builder + interface Builder { + @BindsInstance + Builder setFirebaseApp(FirebaseApp firebaseApp); + + @BindsInstance + Builder setLiteExecutor(@Lightweight Executor liteExecutor); + + @BindsInstance + Builder setBlockingExecutor(@Blocking Executor blockingExecutor); + + ProviderComponent build(); + } + + @Module + abstract class MainModule {} +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderMultiResourceComponent.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderMultiResourceComponent.java new file mode 100644 index 00000000000..93861c937a0 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ProviderMultiResourceComponent.java @@ -0,0 +1,57 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import androidx.annotation.NonNull; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Multi-resource container for RecaptchaEnterpriseAppCheckProvider */ +@Singleton +public final class ProviderMultiResourceComponent { + private final RecaptchaEnterpriseAppCheckProviderFactory providerFactory; + + private final Map instances = + new ConcurrentHashMap<>(); + + @Inject + ProviderMultiResourceComponent(RecaptchaEnterpriseAppCheckProviderFactory providerFactory) { + this.providerFactory = providerFactory; + } + + @NonNull + public RecaptchaEnterpriseAppCheckProvider get(@NonNull String siteKey) { + RecaptchaEnterpriseAppCheckProvider provider = instances.get(siteKey); + if (provider == null) { + synchronized (instances) { + provider = instances.get(siteKey); + if (provider == null) { + provider = providerFactory.create(siteKey); + instances.put(siteKey, provider); + } + } + } + return provider; + } + + @AssistedFactory + interface RecaptchaEnterpriseAppCheckProviderFactory { + RecaptchaEnterpriseAppCheckProvider create(@Assisted String siteKey); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java new file mode 100644 index 00000000000..961ab7defa8 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProvider.java @@ -0,0 +1,142 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import android.app.Application; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.recaptcha.Recaptcha; +import com.google.android.recaptcha.RecaptchaAction; +import com.google.android.recaptcha.RecaptchaTasksClient; +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 dagger.assisted.Assisted; +import dagger.assisted.AssistedInject; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * An implementation of {@link AppCheckProvider} that uses reCAPTCHA Enterprise for device + * attestation. + * + *

This class orchestrates the flow: + * + *

    + *
  1. Obtain a reCAPTCHA token via {@code RecaptchaTasksClient}. + *
  2. Exchange the reCAPTCHA token with the Firebase App Check backend to receive a Firebase App + * Check token. + *
+ */ +public class RecaptchaEnterpriseAppCheckProvider implements AppCheckProvider { + + private final RecaptchaAction recaptchaAction = RecaptchaAction.custom("fire_app_check"); + private volatile Task recaptchaTasksClientTask; + private final Executor liteExecutor; + private final Executor blockingExecutor; + private final RetryManager retryManager; + private final NetworkClient networkClient; + private String siteKey; + private Application application; + private static final String TAG = "rCEAppCheckProvider"; + + @AssistedInject + public RecaptchaEnterpriseAppCheckProvider( + @NonNull FirebaseApp firebaseApp, + @Assisted @NonNull String siteKey, + @Lightweight Executor liteExecutor, + @Blocking Executor blockingExecutor) { + this.application = (Application) firebaseApp.getApplicationContext(); + this.siteKey = siteKey; + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; + this.retryManager = new RetryManager(); + this.networkClient = new NetworkClient(firebaseApp); + } + + @VisibleForTesting + RecaptchaEnterpriseAppCheckProvider( + @Lightweight Executor liteExecutor, + @Blocking Executor blockingExecutor, + @NonNull RetryManager retryManager, + @NonNull NetworkClient networkClient, + @NonNull RecaptchaTasksClient recaptchaTasksClient) { + this.liteExecutor = liteExecutor; + this.blockingExecutor = blockingExecutor; + this.retryManager = retryManager; + this.networkClient = networkClient; + this.recaptchaTasksClientTask = Tasks.forResult(recaptchaTasksClient); + } + + public void initializeRecaptchaClient() { + if (recaptchaTasksClientTask == null) { + synchronized (this) { + if (recaptchaTasksClientTask == null) { + Log.d(TAG, "Initializing RecaptchaTasksClient for siteKey: " + siteKey); + recaptchaTasksClientTask = Recaptcha.fetchTaskClient(application, siteKey); + } + } + } + } + + @NonNull + @Override + public Task getToken() { + return getRecaptchaEnterpriseAttestation() + .onSuccessTask( + liteExecutor, + recaptchaEnterpriseToken -> { + ExchangeRecaptchaEnterpriseTokenRequest request = + new ExchangeRecaptchaEnterpriseTokenRequest(recaptchaEnterpriseToken); + return Tasks.call( + blockingExecutor, + () -> + networkClient.exchangeAttestationForAppCheckToken( + request.toJsonString().getBytes(StandardCharsets.UTF_8), + NetworkClient.RECAPTCHA_ENTERPRISE, + retryManager)); + }) + .onSuccessTask( + liteExecutor, + appCheckTokenResponse -> + Tasks.forResult( + DefaultAppCheckToken.constructFromAppCheckTokenResponse( + appCheckTokenResponse))); + } + + @NonNull + private Task getRecaptchaEnterpriseAttestation() { + return recaptchaTasksClientTask.continueWithTask( + blockingExecutor, + task -> { + if (task.isSuccessful()) { + RecaptchaTasksClient client = task.getResult(); + return client.executeTask(recaptchaAction); + } else { + Log.w(TAG, "Recaptcha task failed", task.getException()); + return Tasks.forException((Objects.requireNonNull(task.getException()))); + } + }); + } +} diff --git a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/FirebaseDynamicLinksImplConstants.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java similarity index 67% rename from firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/FirebaseDynamicLinksImplConstants.java rename to appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java index c9da60e96b8..baae7a935b7 100644 --- a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/FirebaseDynamicLinksImplConstants.java +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/main/java/com/google/firebase/appcheck/recaptchaenterprise/internal/package-info.java @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2025 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,10 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// THIS FILE IS AUTO GENERATED. Do not modify. -package com.google.firebase.dynamiclinks.internal; - -interface FirebaseDynamicLinksImplConstants { - int GET_DYNAMIC_LINK_METHOD_KEY = 13201; - int CREATE_SHORT_DYNAMIC_LINK_METHOD_KEY = 13202; -} +/** @hide */ +package com.google.firebase.appcheck.recaptchaenterprise.internal; diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java new file mode 100644 index 00000000000..1d3ddadca85 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/FirebaseAppCheckRecaptchaEnterpriseRegistrarTest.java @@ -0,0 +1,38 @@ +// Copyright 2025 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.recaptchaenterprise; + +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 FirebaseAppCheckRecaptchaEnterpriseRegistrar}. */ +@RunWith(RobolectricTestRunner.class) +public class FirebaseAppCheckRecaptchaEnterpriseRegistrarTest { + @Test + public void testGetComponents() { + FirebaseAppCheckRecaptchaEnterpriseRegistrar registrar = + new FirebaseAppCheckRecaptchaEnterpriseRegistrar(); + List> components = registrar.getComponents(); + assertThat(components).isNotEmpty(); + assertThat(components).hasSize(2); + Component firebaseExecutorsComponent = components.get(0); + assertThat(firebaseExecutorsComponent.isLazy()).isTrue(); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java new file mode 100644 index 00000000000..865b69e409c --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/RecaptchaEnterpriseAppCheckProviderFactoryTest.java @@ -0,0 +1,84 @@ +// Copyright 2025 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.recaptchaenterprise; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.appcheck.AppCheckProvider; +import com.google.firebase.appcheck.recaptchaenterprise.internal.ProviderMultiResourceComponent; +import com.google.firebase.appcheck.recaptchaenterprise.internal.RecaptchaEnterpriseAppCheckProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Tests for {@link RecaptchaEnterpriseAppCheckProviderFactory}. */ +@RunWith(MockitoJUnitRunner.class) +public class RecaptchaEnterpriseAppCheckProviderFactoryTest { + static final String SITE_KEY_1 = "siteKey1"; + + @Mock private FirebaseApp mockFirebaseApp; + @Mock private ProviderMultiResourceComponent mockComponent; + @Mock private RecaptchaEnterpriseAppCheckProvider mockProvider; + + @Before + public void setUp() { + when(mockFirebaseApp.get(eq(ProviderMultiResourceComponent.class))).thenReturn(mockComponent); + when(mockComponent.get(anyString())).thenReturn(mockProvider); + } + + @Test + public void getInstance_nonNullSiteKey_returnsNonNullInstance() { + RecaptchaEnterpriseAppCheckProviderFactory factory = + RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1); + assertNotNull(factory); + } + + @Test + public void getInstance_nullSiteKey_expectThrows() { + assertThrows( + NullPointerException.class, + () -> RecaptchaEnterpriseAppCheckProviderFactory.getInstance(null)); + } + + @Test + public void create_nonNullFirebaseApp_returnsRecaptchaEnterpriseAppCheckProvider() { + RecaptchaEnterpriseAppCheckProviderFactory factory = + RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1); + AppCheckProvider provider = factory.create(mockFirebaseApp); + assertNotNull(provider); + assertEquals(RecaptchaEnterpriseAppCheckProvider.class, provider.getClass()); + } + + @Test + public void create_callMultipleTimes_providerIsInitializedOnlyOnce() { + RecaptchaEnterpriseAppCheckProviderFactory factory = + RecaptchaEnterpriseAppCheckProviderFactory.getInstance(SITE_KEY_1); + + factory.create(mockFirebaseApp); + factory.create(mockFirebaseApp); + factory.create(mockFirebaseApp); + verify(mockProvider, times(1)).initializeRecaptchaClient(); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java new file mode 100644 index 00000000000..20e48b7eb46 --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/ExchangeRecaptchaEnterpriseTokenRequestTest.java @@ -0,0 +1,39 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import static com.google.common.truth.Truth.assertThat; + +import org.json.JSONObject; +import org.junit.Test; + +/** Tests for {@link ExchangeRecaptchaEnterpriseTokenRequest}. */ +public class ExchangeRecaptchaEnterpriseTokenRequestTest { + private static final String RECAPTCHA_ENTERPRISE_TOKEN = "recaptchaEnterpriseToken"; + + @Test + public void toJsonString_expectSerialized() throws Exception { + ExchangeRecaptchaEnterpriseTokenRequest exchangeRecaptchaEnterpriseTokenRequest = + new ExchangeRecaptchaEnterpriseTokenRequest(RECAPTCHA_ENTERPRISE_TOKEN); + + String jsonString = exchangeRecaptchaEnterpriseTokenRequest.toJsonString(); + JSONObject jsonObject = new JSONObject(jsonString); + + assertThat( + jsonObject.getString( + ExchangeRecaptchaEnterpriseTokenRequest.RECAPTCHA_ENTERPRISE_TOKEN_KEY)) + .isEqualTo(RECAPTCHA_ENTERPRISE_TOKEN); + } +} diff --git a/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java new file mode 100644 index 00000000000..19cbdc4957b --- /dev/null +++ b/appcheck/firebase-appcheck-recaptchaenterprise/src/test/java/com/google/firebase/appcheck/recaptchaenterprise/internal/RecaptchaEnterpriseAppCheckProviderTest.java @@ -0,0 +1,164 @@ +// Copyright 2025 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.recaptchaenterprise.internal; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.recaptcha.RecaptchaAction; +import com.google.android.recaptcha.RecaptchaTasksClient; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; +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 com.google.firebase.concurrent.TestOnlyExecutors; +import java.io.IOException; +import java.util.concurrent.Executor; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.LooperMode; + +/** Tests for {@link RecaptchaEnterpriseAppCheckProvider}. */ +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +@LooperMode(LooperMode.Mode.LEGACY) +public class RecaptchaEnterpriseAppCheckProviderTest { + private static final String APP_CHECK_TOKEN = "appCheckToken"; + private static final String RECAPTCHA_ENTERPRISE_TOKEN = "recaptchaEnterpriseToken"; + private final Executor liteExecutor = MoreExecutors.directExecutor(); + private final Executor blockingExecutor = MoreExecutors.directExecutor(); + private final String siteKey = "siteKey"; + + @Mock private NetworkClient mockNetworkClient; + @Mock FirebaseApp mockFirebaseApp; + @Mock RecaptchaTasksClient mockRecaptchaTasksClient; + @Mock RetryManager mockRetryManager; + + @Captor private ArgumentCaptor recaptchaActionCaptor; + @Captor private ArgumentCaptor requestCaptor; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testPublicConstructor_nullFirebaseApp_expectThrows() { + assertThrows( + NullPointerException.class, + () -> + new RecaptchaEnterpriseAppCheckProvider( + null, siteKey, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking())); + } + + @Test + public void testPublicConstructor_nullSiteKey_expectThrows() { + assertThrows( + NullPointerException.class, + () -> + new RecaptchaEnterpriseAppCheckProvider( + mockFirebaseApp, null, TestOnlyExecutors.lite(), TestOnlyExecutors.blocking())); + } + + @Test + public void getToken_onSuccess_setsTaskResult() throws Exception { + when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class))) + .thenReturn(Tasks.forResult(RECAPTCHA_ENTERPRISE_TOKEN)); + String jsonResponse = + new JSONObject().put("token", APP_CHECK_TOKEN).put("ttl", 3600).toString(); + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(byte[].class), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager))) + .thenReturn(AppCheckTokenResponse.fromJsonString(jsonResponse)); + + RecaptchaEnterpriseAppCheckProvider provider = + new RecaptchaEnterpriseAppCheckProvider( + liteExecutor, + blockingExecutor, + mockRetryManager, + mockNetworkClient, + mockRecaptchaTasksClient); + Task task = provider.getToken(); + + assertThat(task.isSuccessful()).isTrue(); + AppCheckToken token = task.getResult(); + assertThat(token).isInstanceOf(DefaultAppCheckToken.class); + assertThat(token.getToken()).isEqualTo(APP_CHECK_TOKEN); + + verify(mockRecaptchaTasksClient).executeTask(recaptchaActionCaptor.capture()); + assertThat(recaptchaActionCaptor.getValue().getAction()).isEqualTo("fire_app_check"); + verify(mockNetworkClient) + .exchangeAttestationForAppCheckToken( + requestCaptor.capture(), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager)); + } + + @Test + public void getToken_recaptchaFails_returnException() { + Exception exception = new Exception("Recaptcha error"); + when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class))) + .thenReturn(Tasks.forException(exception)); + + RecaptchaEnterpriseAppCheckProvider provider = + new RecaptchaEnterpriseAppCheckProvider( + liteExecutor, + blockingExecutor, + mockRetryManager, + mockNetworkClient, + mockRecaptchaTasksClient); + Task task = provider.getToken(); + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isEqualTo(exception); + } + + @Test + public void getToken_networkFails_returnException() + throws FirebaseException, JSONException, IOException { + when(mockRecaptchaTasksClient.executeTask(any(RecaptchaAction.class))) + .thenReturn(Tasks.forResult(RECAPTCHA_ENTERPRISE_TOKEN)); + Exception exception = new IOException("Network error"); + when(mockNetworkClient.exchangeAttestationForAppCheckToken( + any(byte[].class), eq(NetworkClient.RECAPTCHA_ENTERPRISE), eq(mockRetryManager))) + .thenThrow(exception); + + RecaptchaEnterpriseAppCheckProvider provider = + new RecaptchaEnterpriseAppCheckProvider( + liteExecutor, + blockingExecutor, + mockRetryManager, + mockNetworkClient, + mockRecaptchaTasksClient); + Task task = provider.getToken(); + assertThat(task.isSuccessful()).isFalse(); + assertThat(task.getException()).isEqualTo(exception); + } +} diff --git a/appcheck/firebase-appcheck/CHANGELOG.md b/appcheck/firebase-appcheck/CHANGELOG.md index 19e29699175..88d12d6afc3 100644 --- a/appcheck/firebase-appcheck/CHANGELOG.md +++ b/appcheck/firebase-appcheck/CHANGELOG.md @@ -1,141 +1,154 @@ # Unreleased +# 19.0.1 + +- [changed] Bumped internal dependencies. + +# 19.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. +- [removed] **Breaking Change**: Stopped releasing the deprecated Kotlin extensions (KTX) module and + removed it from the Firebase Android BoM. Instead, use the KTX APIs from the main module. For + details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration). # 18.0.0 -* [changed] Bump internal dependencies -* [changed] Internal support for `SafetyNet` has been dropped, as the [SafetyNet Attestation API -has been deprecated.](https://developer.android.com/privacy-and-security/safetynet/deprecation-timeline#safetynet_attestation_deprecation_timeline) +- [changed] Bump internal dependencies +- [changed] Internal support for `SafetyNet` has been dropped, as the + [SafetyNet Attestation API has been deprecated.](https://developer.android.com/privacy-and-security/safetynet/deprecation-timeline#safetynet_attestation_deprecation_timeline) ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 17.1.2 -* [changed] Bump internal dependencies. +- [changed] Bump internal dependencies. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 17.1.1 -* [fixed] Fixed a bug causing internal tests to depend directly on `firebase-common`. -* [fixed] Fixed client-side throttling in Play Integrity flows. +- [fixed] Fixed a bug causing internal tests to depend directly on `firebase-common`. +- [fixed] Fixed client-side throttling in Play Integrity flows. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 17.1.0 -* [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-appcheck-ktx` - to `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package. - For details, see the + +- [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-appcheck-ktx` to + `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package. For + details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) -* [deprecated] All the APIs from `com.google.firebase:firebase-appcheck-ktx` have been added to - `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package, - and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-appcheck-ktx` are - now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the +- [deprecated] All the APIs from `com.google.firebase:firebase-appcheck-ktx` have been added to + `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package, and all + the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-appcheck-ktx` are now + deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 17.0.1 -* [changed] Internal updates to allow Firebase SDKs to obtain limited-use tokens. + +- [changed] Internal updates to allow Firebase SDKs to obtain limited-use tokens. # 17.0.0 -* [feature] Added [`getLimitedUseAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getLimitedUseAppCheckToken()) - for obtaining limited-use tokens for protecting non-Firebase backends. +- [feature] Added + [`getLimitedUseAppCheckToken()`]() + for obtaining limited-use tokens for protecting non-Firebase backends. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 16.1.2 -* [unchanged] Updated to keep [app_check] SDK versions aligned. +- [unchanged] Updated to keep [app_check] SDK versions aligned. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 16.1.1 -* [changed] Migrated [app_check] SDKs to use standard Firebase executors. - (GitHub [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} - and - [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) -* [changed] Moved Task continuations off the main thread. - (GitHub [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) +- [changed] Migrated [app_check] SDKs to use standard Firebase executors. (GitHub + [#4431](//github.com/firebase/firebase-android-sdk/issues/4431){: .external} and + [#4449](//github.com/firebase/firebase-android-sdk/issues/4449){: .external}) +- [changed] Moved Task continuations off the main thread. (GitHub + [#4453](//github.com/firebase/firebase-android-sdk/issues/4453){: .external}) ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has no additional updates. # 16.1.0 -* [unchanged] Updated to accommodate the release of the updated - [app_check] Kotlin extensions library. +- [unchanged] Updated to accommodate the release of the updated [app_check] Kotlin extensions + library. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appcheck` library. The Kotlin extensions library has the following -additional updates: - -* [feature] Firebase now supports Kotlin coroutines. - With this release, we added - [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: .external} - to `firebase-appcheck-ktx` as a transitive dependency, which exposes the + +The Kotlin extensions library transitively includes the updated `firebase-appcheck` library. The +Kotlin extensions library has the following additional updates: + +- [feature] Firebase now supports Kotlin coroutines. With this release, we added + [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: + .external} to `firebase-appcheck-ktx` as a transitive dependency, which exposes the `Task.await()` suspend function to convert a - [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin - coroutine. + [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin coroutine. # 16.0.1 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). + +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). # 16.0.0 -* [changed] [app_check] has exited beta and is now generally available for - use. -* [feature] Added support for - [Play Integrity](https://developer.android.com/google/play/integrity) as an - attestation provider. + +- [changed] [app_check] has exited beta and is now generally available for use. +- [feature] Added support for [Play Integrity](https://developer.android.com/google/play/integrity) + as an attestation provider. # 16.0.0-beta06 -* [fixed] Fixed a bug in the [app_check] token refresh flow when using a - custom provider. + +- [fixed] Fixed a bug in the [app_check] token refresh flow when using a custom provider. # 16.0.0-beta05 -* [changed] Internal improvements. + +- [changed] Internal improvements. # 16.0.0-beta04 -* [changed] Improved error handling logic by minimizing the amount of requests - that are unlikely to succeed. -* [fixed] Fixed heartbeat reporting. + +- [changed] Improved error handling logic by minimizing the amount of requests that are unlikely to + succeed. +- [fixed] Fixed heartbeat reporting. # 16.0.0-beta03 -* [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to - [app_check] network calls. + +- [changed] Added `X-Android-Package` and `X-Android-Cert` request headers to [app_check] network + calls. # 16.0.0-beta02 -* [feature] Added [`getAppCheckToken()`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck#getAppCheckToken(boolean)), + +- [feature] Added + [`getAppCheckToken()`](), [`AppCheckTokenListener`](/docs/reference/android/com/google/firebase/appcheck/FirebaseAppCheck.AppCheckListener), - and associated setters and removers for developers to request and observe - changes to the [app_check] token. + and associated setters and removers for developers to request and observe changes to the + [app_check] token. # 16.0.0-beta01 -* [feature] Initial beta release of the [app_check] SDK with abuse reduction - features. +- [feature] Initial beta release of the [app_check] SDK with abuse reduction features. diff --git a/appcheck/firebase-appcheck/api.txt b/appcheck/firebase-appcheck/api.txt index fe214cd0b66..557c950dd0e 100644 --- a/appcheck/firebase-appcheck/api.txt +++ b/appcheck/firebase-appcheck/api.txt @@ -41,14 +41,3 @@ package com.google.firebase.appcheck { } -package com.google.firebase.appcheck.ktx { - - public final class FirebaseAppCheckKt { - method @Deprecated public static com.google.firebase.appcheck.FirebaseAppCheck appCheck(com.google.firebase.ktx.Firebase, com.google.firebase.FirebaseApp app); - method @Deprecated public static operator String component1(com.google.firebase.appcheck.AppCheckToken); - method @Deprecated public static operator long component2(com.google.firebase.appcheck.AppCheckToken); - method @Deprecated public static com.google.firebase.appcheck.FirebaseAppCheck getAppCheck(com.google.firebase.ktx.Firebase); - } - -} - diff --git a/appcheck/firebase-appcheck/firebase-appcheck.gradle b/appcheck/firebase-appcheck/firebase-appcheck.gradle index 8bb131495f3..c43505f766a 100644 --- a/appcheck/firebase-appcheck/firebase-appcheck.gradle +++ b/appcheck/firebase-appcheck/firebase-appcheck.gradle @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id 'firebase-library' id("kotlin-android") @@ -43,18 +45,19 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { jvmTarget = "1.8" } testOptions.unitTests.includeAndroidResources = true } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { javadocClasspath libs.autovalue.annotations api libs.playservices.tasks - api 'com.google.firebase:firebase-annotations:16.2.0' + api libs.firebase.annotations api "com.google.firebase:firebase-appcheck-interop:17.1.0" - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-components:18.0.0") + api(libs.firebase.common) + api(libs.firebase.components) implementation libs.androidx.annotation implementation libs.playservices.base @@ -70,7 +73,6 @@ dependencies { testImplementation libs.junit testImplementation libs.junit testImplementation libs.mockito.core - testImplementation libs.mockito.mockito.inline testImplementation libs.robolectric androidTestImplementation project(':appcheck:firebase-appcheck') @@ -85,5 +87,4 @@ dependencies { androidTestImplementation libs.truth androidTestImplementation libs.junit androidTestImplementation libs.mockito.core - androidTestImplementation libs.mockito.mockito.inline } diff --git a/appcheck/firebase-appcheck/gradle.properties b/appcheck/firebase-appcheck/gradle.properties index 9b7be4891d1..04d7bc444aa 100644 --- a/appcheck/firebase-appcheck/gradle.properties +++ b/appcheck/firebase-appcheck/gradle.properties @@ -1,2 +1,2 @@ -version=18.0.1 -latestReleasedVersion=18.0.0 +version=19.0.2 +latestReleasedVersion=19.0.1 diff --git a/appcheck/firebase-appcheck/ktx/api.txt b/appcheck/firebase-appcheck/ktx/api.txt deleted file mode 100644 index da4f6cc18fe..00000000000 --- a/appcheck/firebase-appcheck/ktx/api.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 3.0 diff --git a/appcheck/firebase-appcheck/ktx/gradle.properties b/appcheck/firebase-appcheck/ktx/gradle.properties deleted file mode 100644 index 9eff84e6c72..00000000000 --- a/appcheck/firebase-appcheck/ktx/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -android.enableUnitTestBinaryResources=true diff --git a/appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml b/appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml deleted file mode 100644 index 1b0d1dff1e4..00000000000 --- a/appcheck/firebase-appcheck/ktx/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,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 deleted file mode 100644 index 2989924a2c5..00000000000 --- a/appcheck/firebase-appcheck/ktx/src/androidTest/kotlin/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.ktx - -import androidx.test.core.app.ApplicationProvider -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.appcheck.AppCheckToken -import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app -import com.google.firebase.ktx.initialize -import com.google.firebase.platforminfo.UserAgentPublisher -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -const val APP_ID = "APP_ID" -const val API_KEY = "API_KEY" - -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() - } -} - -@RunWith(AndroidJUnit4ClassRunner::class) -class FirebaseAppCheckTests : BaseTestCase() { - @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 appCheckToken_destructuring_declaration_works() { - val mockAppCheckToken = - object : AppCheckToken() { - override fun getToken(): String = "randomToken" - - override fun getExpireTimeMillis(): Long = 23121997 - } - - val (token, expiration) = mockAppCheckToken - - 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) - } -} diff --git a/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml deleted file mode 100644 index e2af9507263..00000000000 --- a/appcheck/firebase-appcheck/ktx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt b/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt deleted file mode 100644 index 56050655f1f..00000000000 --- a/appcheck/firebase-appcheck/ktx/src/main/kotlin/com/google/firebase/appcheck/ktx/Logging.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 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.ktx - -import androidx.annotation.Keep -import com.google.firebase.appcheck.BuildConfig -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.platforminfo.LibraryVersionComponent - -internal const val LIBRARY_NAME: String = "fire-app-check-ktx" - -/** @suppress */ -@Keep -class FirebaseAppcheckLegacyRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) - } -} diff --git a/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt b/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt deleted file mode 100644 index 969b0230ed5..00000000000 --- a/appcheck/firebase-appcheck/src/androidTest/java/com/google/firebase/appcheck/ktx/FirebaseAppCheckTests.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.ktx - -import androidx.test.core.app.ApplicationProvider -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.appcheck.AppCheckToken -import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app -import com.google.firebase.ktx.initialize -import com.google.firebase.platforminfo.UserAgentPublisher -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -const val APP_ID = "APP_ID" -const val API_KEY = "API_KEY" - -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() - } -} - -@RunWith(AndroidJUnit4ClassRunner::class) -class FirebaseAppCheckTests : BaseTestCase() { - @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 appCheckToken_destructuring_declaration_works() { - val mockAppCheckToken = - object : AppCheckToken() { - override fun getToken(): String = "randomToken" - - override fun getExpireTimeMillis(): Long = 23121997 - } - - val (token, expiration) = mockAppCheckToken - - assertThat(token).isEqualTo(mockAppCheckToken.token) - assertThat(expiration).isEqualTo(mockAppCheckToken.expireTimeMillis) - } -} - -@RunWith(AndroidJUnit4ClassRunner::class) -class LibraryVersionTest : BaseTestCase() { - @Test - fun libraryRegistrationAtRuntime() { - val publisher = Firebase.app.get(UserAgentPublisher::class.java) - } -} diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java index 410f59290cb..fa91e6f8323 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java @@ -41,6 +41,7 @@ import java.lang.annotation.RetentionPolicy; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import org.json.JSONException; /** @@ -57,9 +58,10 @@ public class NetworkClient { "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangePlayIntegrityToken?key=%s"; private static final String PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE = "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:generatePlayIntegrityChallenge?key=%s"; + private static final String RECAPTCHA_ENTERPRISE_URL_TEMPLATE = + "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeRecaptchaEnterpriseToken?key=%s"; private static final String CONTENT_TYPE = "Content-Type"; private static final String APPLICATION_JSON = "application/json"; - private static final String UTF_8 = "UTF-8"; @VisibleForTesting static final String X_FIREBASE_CLIENT = "X-Firebase-Client"; @VisibleForTesting static final String X_ANDROID_PACKAGE = "X-Android-Package"; @VisibleForTesting static final String X_ANDROID_CERT = "X-Android-Cert"; @@ -71,12 +73,13 @@ public class NetworkClient { private final Provider heartBeatControllerProvider; @Retention(RetentionPolicy.SOURCE) - @IntDef({UNKNOWN, DEBUG, PLAY_INTEGRITY}) + @IntDef({UNKNOWN, DEBUG, PLAY_INTEGRITY, RECAPTCHA_ENTERPRISE}) public @interface AttestationTokenType {} public static final int UNKNOWN = 0; public static final int DEBUG = 2; public static final int PLAY_INTEGRITY = 3; + public static final int RECAPTCHA_ENTERPRISE = 4; public NetworkClient(@NonNull FirebaseApp firebaseApp) { this( @@ -172,7 +175,8 @@ private String makeNetworkRequest( ? urlConnection.getInputStream() : urlConnection.getErrorStream(); StringBuilder response = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { response.append(line); @@ -236,6 +240,8 @@ private static String getUrlTemplate(@AttestationTokenType int tokenType) { return DEBUG_EXCHANGE_URL_TEMPLATE; case PLAY_INTEGRITY: return PLAY_INTEGRITY_EXCHANGE_URL_TEMPLATE; + case RECAPTCHA_ENTERPRISE: + return RECAPTCHA_ENTERPRISE_URL_TEMPLATE; default: throw new IllegalArgumentException("Unknown token type."); } @@ -246,7 +252,7 @@ HttpURLConnection createHttpUrlConnection(URL url) throws IOException { return (HttpURLConnection) url.openConnection(); } - private static final boolean isResponseSuccess(int responseCode) { + private static boolean isResponseSuccess(int responseCode) { return responseCode >= 200 && responseCode < 300; } } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt deleted file mode 100644 index 15e1f5b2189..00000000000 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.ktx - -import com.google.firebase.FirebaseApp -import com.google.firebase.appcheck.AppCheckToken -import com.google.firebase.appcheck.FirebaseAppCheck -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app - -/** - * Accessing this object for Kotlin apps has changed; see the - * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). - * - * Returns the [FirebaseAppCheck] instance of the default [FirebaseApp]. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -val Firebase.appCheck: FirebaseAppCheck - get() = FirebaseAppCheck.getInstance() - -/** - * Accessing this object for Kotlin apps has changed; see the - * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). - * - * Returns the [FirebaseAppCheck] instance of a given [FirebaseApp]. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) - -/** - * Destructuring declaration for [AppCheckToken] to provide token. - * - * @return the token of the [AppCheckToken] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppCheckToken.component1() = token - -/** - * Destructuring declaration for [AppCheckToken] to provide expireTimeMillis. - * - * @return the expireTimeMillis of the [AppCheckToken] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppCheckToken.component2() = expireTimeMillis - -/** - * @suppress - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-appcheck-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -class FirebaseAppCheckKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = listOf() -} diff --git a/build.gradle.kts b/build.gradle.kts index a10ac0119ea..dab824bdf4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ extra["targetSdkVersion"] = 34 extra["compileSdkVersion"] = 34 -extra["minSdkVersion"] = 21 +extra["minSdkVersion"] = 23 firebaseContinuousIntegration { ignorePaths = @@ -60,6 +60,11 @@ fun Project.applySpotless() { target("*.gradle.kts") // default target for kotlinGradle ktfmt("0.41").googleStyle() } + format("styling") { + target("src/**/*.md", "*.md", "docs/**/*.md") + targetExclude("**/third_party/**", "src/test/resources/**", "release_report.md") + prettier().config(mapOf("printWidth" to 100, "proseWrap" to "always")) + } } } diff --git a/ci/README.md b/ci/README.md index a4b4eb4799b..1948ff3b7e3 100644 --- a/ci/README.md +++ b/ci/README.md @@ -14,6 +14,7 @@ This directory contains tooling used to run Continuous Integration tasks. source ~/.venvs/fireci/bin/activate ``` - At the root of the firebase sdk repo, run + ``` pip3 install -e ./ci/fireci/ ``` @@ -25,8 +26,8 @@ This directory contains tooling used to run Continuous Integration tasks. ## Uninstall -If you run into any issues and need to re-install, or uninstall the package, you can do so -by uninstalling the `fireci` package. +If you run into any issues and need to re-install, or uninstall the package, you can do so by +uninstalling the `fireci` package. ```shell pip3 uninstall fireci -y @@ -34,8 +35,8 @@ pip3 uninstall fireci -y ## Debug -By default, if you're not running `fireci` within the context of CI, the minimum log level is set -to `INFO`. +By default, if you're not running `fireci` within the context of CI, the minimum log level is set to +`INFO`. To manually set the level to `DEBUG`, you can use the `--debug` flag. @@ -43,5 +44,4 @@ To manually set the level to `DEBUG`, you can use the `--debug` flag. fireci --debug clean ``` -> ![NOTE] -> The `--debug` flag must come _before_ the command. +> ![NOTE] The `--debug` flag must come _before_ the command. diff --git a/ci/danger/Gemfile b/ci/danger/Gemfile index 6c0467dd241..b6c6c859b2f 100644 --- a/ci/danger/Gemfile +++ b/ci/danger/Gemfile @@ -2,4 +2,4 @@ # commit Gemfile and Gemfile.lock. source 'https://rubygems.org' -gem 'danger', '8.4.5' +gem 'danger', '9.5.3' diff --git a/ci/danger/Gemfile.lock b/ci/danger/Gemfile.lock index 783002346b0..42d08f0a17a 100644 --- a/ci/danger/Gemfile.lock +++ b/ci/danger/Gemfile.lock @@ -1,86 +1,102 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) + activesupport (8.0.3) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.3.0) claide (1.1.0) claide-plugins (0.9.2) cork nap open4 (~> 1.3) colored2 (3.1.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) cork (0.3.0) colored2 (~> 3.1) - danger (8.4.5) + danger (9.5.3) + base64 (~> 0.2) claide (~> 1.0) claide-plugins (>= 0.9.2) - colored2 (~> 3.1) + colored2 (>= 3.1, < 5) cork (~> 0.1) - faraday (>= 0.9.0, < 2.0) + faraday (>= 0.9.0, < 3.0) faraday-http-cache (~> 2.0) - git (~> 1.7) - kramdown (~> 2.3) + git (>= 1.13, < 3.0) + kramdown (>= 2.5.1, < 3.0) kramdown-parser-gfm (~> 1.0) - no_proxy_fix - octokit (~> 4.7) - terminal-table (>= 1, < 4) - faraday (1.10.1) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-http-cache (2.4.1) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 5) + drb (2.2.3) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-http-cache (2.5.1) faraday (>= 0.8) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - git (1.13.1) + faraday-net_http (3.4.1) + net-http (>= 0.5.0) + git (2.3.3) + activesupport (>= 5.0) addressable (~> 2.8) + process_executer (~> 1.1) rchardet (~> 1.8) - kramdown (2.4.0) - rexml + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.15.1) + kramdown (2.5.1) + rexml (>= 3.3.9) kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - multipart-post (2.2.3) + logger (1.7.0) + minitest (5.25.5) nap (1.1.0) - no_proxy_fix (0.1.2) - octokit (4.25.1) + net-http (0.6.0) + uri + octokit (10.0.0) faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) - public_suffix (5.0.1) - rchardet (1.8.0) - rexml (3.2.8) - strscan (>= 3.0.9) - ruby2_keywords (0.0.5) + process_executer (1.3.0) + pstore (0.2.0) + public_suffix (6.0.2) + rchardet (1.10.0) + rexml (3.4.4) sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - strscan (3.1.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.2.0) + securerandom (0.4.1) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.0.4) PLATFORMS + arm64-darwin-24 ruby DEPENDENCIES - danger (= 8.4.5) + danger (= 9.5.3) BUNDLED WITH - 1.17.2 + 2.7.2 diff --git a/ci/fireci/fireciplugins/binary_size.py b/ci/fireci/fireciplugins/binary_size.py index 6f966a3bfd7..ddb97c3f939 100644 --- a/ci/fireci/fireciplugins/binary_size.py +++ b/ci/fireci/fireciplugins/binary_size.py @@ -55,6 +55,13 @@ def binary_size(pull_request, log, metrics_service_url, access_token): affected_artifacts, all_artifacts = _parse_artifacts() artifacts = affected_artifacts if pull_request else all_artifacts sdks = ','.join(artifacts) + if not sdks: + _logger.info( + "No sdks found whose binary size to measure (" + "pull_request=%s affected_artifacts=%s all_artifacts=%s)", + pull_request, affected_artifacts, all_artifacts + ) + return workdir = 'health-metrics/apk-size' process = gradle.run('assemble', '--continue', gradle.P('sdks', sdks), workdir=workdir, check=False) diff --git a/ci/fireci/fireciplugins/clean.py b/ci/fireci/fireciplugins/clean.py index 9f2cd6af9a5..2238cccab35 100644 --- a/ci/fireci/fireciplugins/clean.py +++ b/ci/fireci/fireciplugins/clean.py @@ -40,7 +40,6 @@ \b $ fireci clean firebase-common - $ fireci clean firebase-common firebase-vertexai Clean all projects: diff --git a/ci/fireci/fireciplugins/macrobenchmark/run/runner.py b/ci/fireci/fireciplugins/macrobenchmark/run/runner.py index a997e66cb49..8cff62ee954 100644 --- a/ci/fireci/fireciplugins/macrobenchmark/run/runner.py +++ b/ci/fireci/fireciplugins/macrobenchmark/run/runner.py @@ -119,7 +119,6 @@ def _process_changed_modules(path: Path) -> List[str]: ":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"], diff --git a/ci/fireci/pyproject.toml b/ci/fireci/pyproject.toml index 8fd3b462353..41f77c976fb 100644 --- a/ci/fireci/pyproject.toml +++ b/ci/fireci/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "fireci" version = "0.1" dependencies = [ - "protobuf==3.20.3", + "protobuf==4.25.8", "click==8.1.7", "google-cloud-storage==2.18.2", "mypy==1.6.0", @@ -14,7 +14,7 @@ dependencies = [ "pandas==1.5.3", "PyGithub==1.58.2", "pystache==0.6.0", - "requests==2.32.2", + "requests==2.32.4", "seaborn==0.12.2", "PyYAML==6.0.1", "termcolor==2.4.0", diff --git a/ci/workflow_summary/README.md b/ci/workflow_summary/README.md index c60558db9d2..d6318224ea2 100644 --- a/ci/workflow_summary/README.md +++ b/ci/workflow_summary/README.md @@ -1,176 +1,190 @@ # `workflow_information.py` Script ## Prerequisites -- [Python](https://www.python.org/) and required packages. - ``` - pip install requests argparse - ``` + +- [Python](https://www.python.org/) and required packages. + ``` + pip install requests argparse + ``` ## Usage -- Collect last `90` days' `Postsubmit` `ci_workflow.yml` workflow runs: - ``` - python workflow_information.py --token ${your_github_toke} --branch master --event push --d 90 - ``` -- Collect last `30` days' `Presubmit` `ci_workflow.yml` workflow runs: - ``` - python workflow_information.py --token ${your_github_toke} --event pull_request --d 30 - ``` +- Collect last `90` days' `Postsubmit` `ci_workflow.yml` workflow runs: + + ``` + python workflow_information.py --token ${your_github_toke} --branch master --event push --d 90 + ``` + +- Collect last `30` days' `Presubmit` `ci_workflow.yml` workflow runs: -- Please refer to `Inputs` section for more use cases, and `Outputs` section for the workflow summary report format. + ``` + python workflow_information.py --token ${your_github_toke} --event pull_request --d 30 + ``` + +- Please refer to `Inputs` section for more use cases, and `Outputs` section for the workflow + summary report format. ## Inputs -- `-o, --repo_owner`: **[Required]** GitHub repo owner, default value is `firebase`. -- `-n, --repo_name`: **[Required]** GitHub repo name, default value is `firebase-android-sdk`. +- `-o, --repo_owner`: **[Required]** GitHub repo owner, default value is `firebase`. -- `-t, --token`: **[Required]** GitHub access token. See [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). +- `-n, --repo_name`: **[Required]** GitHub repo name, default value is `firebase-android-sdk`. -- `-w, --workflow_name`: **[Required]** Workflow filename, default value is `ci_tests.yml`. +- `-t, --token`: **[Required]** GitHub access token. See + [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). -- `-d, --days`: Filter workflows that running in past -d days, default value is `90`. See [retention period for GitHub Actions artifacts and logs](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization). +- `-w, --workflow_name`: **[Required]** Workflow filename, default value is `ci_tests.yml`. -- `-b, --branch`: Filter branch name that workflows run against. +- `-d, --days`: Filter workflows that running in past -d days, default value is `90`. See + [retention period for GitHub Actions artifacts and logs](https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization). -- `-a, --actor`: Filter the actor who triggers the workflow runs. +- `-b, --branch`: Filter branch name that workflows run against. -- `-e, --event`: Filter workflows trigger event, could be one of the following values `['push', 'pull_request', 'issue']`. +- `-a, --actor`: Filter the actor who triggers the workflow runs. -- `-j, --jobs`: Filter workflows jobs, default is `all` (including rerun jobs), could be one of the following values `['latest', 'all']`. +- `-e, --event`: Filter workflows trigger event, could be one of the following values + `['push', 'pull_request', 'issue']`. -- `-f, --folder`: Workflow and job information will be store here, default value is the current datatime. +- `-j, --jobs`: Filter workflows jobs, default is `all` (including rerun jobs), could be one of the + following values `['latest', 'all']`. +- `-f, --folder`: Workflow and job information will be store here, default value is the current + datatime. ## Outputs -- `workflow_summary_report.txt`: a general report contains workflow pass/failure count, running time, etc. - - ``` - 2023-03-03 01:37:07.114500 - Namespace(actor=None, branch=None, days=30, event='pull_request', folder='presubmit_30', jobs='all', repo_name='firebase-android-sdk', repo_owner='firebase', token=${your_github_token}, workflow_name='ci_tests.yml') - - Workflow 'ci_tests.yml' Report: - Workflow Failure Rate:64.77% - Workflow Total Count: 193 (success: 68, failure: 125) - - Workflow Runtime Report: - 161 workflow runs finished without rerun, the average running time: 0:27:24.745342 - Including: - 56 passed workflow runs, with average running time: 0:17:29.214286 - 105 failed workflow runs, with average running time: 0:32:42.361905 - - 32 runs finished with rerun, the average running time: 1 day, 3:57:53.937500 - The running time for each workflow reruns are: - ['1 day, 2:24:32', '3:35:54', '3:19:14', '4 days, 6:10:50', '15:33:39', '1:57:21', '1:13:12', '1:55:18', '12 days, 21:51:29', '0:48:48', '0:45:28', '1:40:21', '2 days, 1:46:35', '19:47:16', '0:45:49', '2:22:36', '0:25:22', '0:55:30', '1:40:32', '1:10:05', '20:08:38', '0:31:03', '5 days, 9:19:25', '5:10:44', '1:20:57', '0:28:47', '1:52:44', '20:19:17', '0:35:15', '21:31:07', '3 days, 1:06:44', '3 days, 2:18:14'] - - Job Failure Report: - Unit Tests (:firebase-storage): - Failure Rate:54.61% - Total Count: 152 (success: 69, failure: 83) - Unit Tests (:firebase-messaging): - Failure Rate:35.37% - Total Count: 147 (success: 95, failure: 52) - ``` - - -- Intermediate file `workflow_summary.json`: contains all the workflow runs and job information attached to each workflow. - - ``` - { - 'workflow_name':'ci_tests.yml', - 'total_count':81, - 'success_count':32, - 'failure_count':49, - 'created':'>2022-11-30T23:15:04Z', - 'workflow_runs':[ +- `workflow_summary_report.txt`: a general report contains workflow pass/failure count, running + time, etc. + + ``` + 2023-03-03 01:37:07.114500 + Namespace(actor=None, branch=None, days=30, event='pull_request', folder='presubmit_30', jobs='all', repo_name='firebase-android-sdk', repo_owner='firebase', token=${your_github_token}, workflow_name='ci_tests.yml') + + Workflow 'ci_tests.yml' Report: + Workflow Failure Rate:64.77% + Workflow Total Count: 193 (success: 68, failure: 125) + + Workflow Runtime Report: + 161 workflow runs finished without rerun, the average running time: 0:27:24.745342 + Including: + 56 passed workflow runs, with average running time: 0:17:29.214286 + 105 failed workflow runs, with average running time: 0:32:42.361905 + + 32 runs finished with rerun, the average running time: 1 day, 3:57:53.937500 + The running time for each workflow reruns are: + ['1 day, 2:24:32', '3:35:54', '3:19:14', '4 days, 6:10:50', '15:33:39', '1:57:21', '1:13:12', '1:55:18', '12 days, 21:51:29', '0:48:48', '0:45:28', '1:40:21', '2 days, 1:46:35', '19:47:16', '0:45:49', '2:22:36', '0:25:22', '0:55:30', '1:40:32', '1:10:05', '20:08:38', '0:31:03', '5 days, 9:19:25', '5:10:44', '1:20:57', '0:28:47', '1:52:44', '20:19:17', '0:35:15', '21:31:07', '3 days, 1:06:44', '3 days, 2:18:14'] + + Job Failure Report: + Unit Tests (:firebase-storage): + Failure Rate:54.61% + Total Count: 152 (success: 69, failure: 83) + Unit Tests (:firebase-messaging): + Failure Rate:35.37% + Total Count: 147 (success: 95, failure: 52) + ``` + +- Intermediate file `workflow_summary.json`: contains all the workflow runs and job information + attached to each workflow. + + ``` + { + 'workflow_name':'ci_tests.yml', + 'total_count':81, + 'success_count':32, + 'failure_count':49, + 'created':'>2022-11-30T23:15:04Z', + 'workflow_runs':[ + { + 'workflow_id':4296343867, + 'conclusion':'failure', + 'head_branch':'master', + 'actor':'vkryachko', + 'created_at':'2023-02-28T18:47:40Z', + 'updated_at':'2023-02-28T19:20:16Z', + 'run_started_at':'2023-02-28T18:47:40Z', + 'run_attempt':1, + 'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867', + 'jobs_url':'https://api.github.com/repos/firebase/firebase-android-sdk/actions/runs/4296343867/jobs', + 'jobs':{ + 'total_count':95, + 'success_count':92, + 'failure_count':3, + 'job_runs':[ + { + 'job_id':11664775180, + 'job_name':'Determine changed modules', + 'conclusion':'success', + 'created_at':'2023-02-28T18:47:42Z', + 'started_at':'2023-02-28T18:47:50Z', + 'completed_at':'2023-02-28T18:50:11Z', + 'run_attempt': 1, + 'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487936863', + } + ] + } + } + ] + } + ``` + +- Intermediate file `job_summary.json`: contains all the job runs organized by job name. + ``` + { + 'Unit Test Results':{ # job name + 'total_count':17, + 'success_count':7, + 'failure_count':10, + 'failure_jobs':[ # data structure is the same as same as workflow_summary['workflow_runs']['job_runs'] { - 'workflow_id':4296343867, + 'job_id':11372664143, + 'job_name':'Unit Test Results', 'conclusion':'failure', - 'head_branch':'master', - 'actor':'vkryachko', - 'created_at':'2023-02-28T18:47:40Z', - 'updated_at':'2023-02-28T19:20:16Z', - 'run_started_at':'2023-02-28T18:47:40Z', - 'run_attempt':1, - 'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867', - 'jobs_url':'https://api.github.com/repos/firebase/firebase-android-sdk/actions/runs/4296343867/jobs', - 'jobs':{ - 'total_count':95, - 'success_count':92, - 'failure_count':3, - 'job_runs':[ - { - 'job_id':11664775180, - 'job_name':'Determine changed modules', - 'conclusion':'success', - 'created_at':'2023-02-28T18:47:42Z', - 'started_at':'2023-02-28T18:47:50Z', - 'completed_at':'2023-02-28T18:50:11Z', - 'run_attempt': 1, - 'html_url':'https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487936863', - } - ] - } + 'created_at':'2023-02-15T22:02:06Z', + 'started_at':'2023-02-15T22:02:06Z', + 'completed_at':'2023-02-15T22:02:06Z', + 'run_attempt': 1, + 'html_url':'https://github.com/firebase/firebase-android-sdk/runs/11372664143', } ] } - ``` - -- Intermediate file `job_summary.json`: contains all the job runs organized by job name. - ``` - { - 'Unit Test Results':{ # job name - 'total_count':17, - 'success_count':7, - 'failure_count':10, - 'failure_jobs':[ # data structure is the same as same as workflow_summary['workflow_runs']['job_runs'] - { - 'job_id':11372664143, - 'job_name':'Unit Test Results', - 'conclusion':'failure', - 'created_at':'2023-02-15T22:02:06Z', - 'started_at':'2023-02-15T22:02:06Z', - 'completed_at':'2023-02-15T22:02:06Z', - 'run_attempt': 1, - 'html_url':'https://github.com/firebase/firebase-android-sdk/runs/11372664143', - } - ] - } - } - ``` - + } + ``` # `collect_ci_test_logs.py` Script ## Usage -- Collect `ci_test.yml` job failure logs from `workflow_information.py` script's intermediate file: - ``` - python collect_ci_test_logs.py --token ${github_toke} --folder ${folder} - ``` + +- Collect `ci_test.yml` job failure logs from `workflow_information.py` script's intermediate file: + ``` + python collect_ci_test_logs.py --token ${github_toke} --folder ${folder} + ``` ## Inputs -- `-t, --token`: **[Required]** GitHub access token. See [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). +- `-t, --token`: **[Required]** GitHub access token. See + [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). -- `-f, --folder`: **[Required]** Folder that store intermediate files generated by `workflow_information.py`. `ci_workflow.yml` job failure logs will also be stored here. +- `-f, --folder`: **[Required]** Folder that store intermediate files generated by + `workflow_information.py`. `ci_workflow.yml` job failure logs will also be stored here. ## Outputs -- `${job name}.log`: contains job failure rate, list all failed job links and failure logs. - ``` - Unit Tests (:firebase-storage): - Failure rate:40.00% - Total count: 20 (success: 12, failure: 8) - Failed jobs: - - https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487989874 - firebase-storage:testDebugUnitTest - Task :firebase-storage:testDebugUnitTest - 2023-02-28T18:54:38.1333725Z - 2023-02-28T18:54:38.1334278Z com.google.firebase.storage.DownloadTest > streamDownloadWithResumeAndCancel FAILED - 2023-02-28T18:54:38.1334918Z org.junit.ComparisonFailure at DownloadTest.java:190 - 2023-02-28T18:57:20.3329130Z - 2023-02-28T18:57:20.3330165Z 112 tests completed, 1 failed - 2023-02-28T18:57:20.5329189Z - 2023-02-28T18:57:20.5330505Z > Task :firebase-storage:testDebugUnitTest FAILED - ``` +- `${job name}.log`: contains job failure rate, list all failed job links and failure logs. + + ``` + Unit Tests (:firebase-storage): + Failure rate:40.00% + Total count: 20 (success: 12, failure: 8) + Failed jobs: + + https://github.com/firebase/firebase-android-sdk/actions/runs/4296343867/jobs/7487989874 + firebase-storage:testDebugUnitTest + Task :firebase-storage:testDebugUnitTest + 2023-02-28T18:54:38.1333725Z + 2023-02-28T18:54:38.1334278Z com.google.firebase.storage.DownloadTest > streamDownloadWithResumeAndCancel FAILED + 2023-02-28T18:54:38.1334918Z org.junit.ComparisonFailure at DownloadTest.java:190 + 2023-02-28T18:57:20.3329130Z + 2023-02-28T18:57:20.3330165Z 112 tests completed, 1 failed + 2023-02-28T18:57:20.5329189Z + 2023-02-28T18:57:20.5330505Z > Task :firebase-storage:testDebugUnitTest FAILED + ``` diff --git a/contributor-docs/README.md b/contributor-docs/README.md index c7b37f2b6f3..414b545bf72 100644 --- a/contributor-docs/README.md +++ b/contributor-docs/README.md @@ -5,8 +5,9 @@ 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. +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? diff --git a/contributor-docs/best_practices/dependency_injection.md b/contributor-docs/best_practices/dependency_injection.md index 3b5de828998..1b900899bdb 100644 --- a/contributor-docs/best_practices/dependency_injection.md +++ b/contributor-docs/best_practices/dependency_injection.md @@ -5,38 +5,43 @@ 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: +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 +- 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`. +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). +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). +{: .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/) +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. +{: .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. +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: @@ -56,10 +61,12 @@ dependencies { ## 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`. +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`. +This makes it a natural fit to get all inter-sdk dependencies and instatiate the Dagger component +inside the `ComponentRegistrar`. ```kotlin class MyRegistrar : ComponentRegistrar { @@ -122,15 +129,17 @@ class MySdkInteropAdapter @Inject constructor(private val interop: com.google.fi ## 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`. +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. +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: +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. @@ -143,7 +152,7 @@ flowchart LR direction BT subgraph GlobalComponents[Outside of SDK] direction LR - + FirebaseOptions SomeInterop Executor["@Background Executor"] @@ -155,7 +164,7 @@ flowchart LR SomeImpl -.-> SomeInterop SomeImpl -.-> Executor end - + subgraph Default["@DbScope SDK(default)"] MainClassDefault[FirebaseDatabase] --> SomeImpl SomeOtherImplDefault[SomeOtherImpl] -.-> FirebaseOptions @@ -169,7 +178,7 @@ flowchart LR end end end - + classDef green fill:#4db6ac classDef blue fill:#1a73e8 class GlobalComponents green @@ -178,9 +187,11 @@ flowchart LR class MyDbName blue ``` -As you can see above, `DatabaseMultiDb` and `SomeImpl` are singletons, while `FirebaseDatabase` and `SomeOtherImpl` are scoped per `database name`. +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). +It can be easily achieved with the help of +[Dagger's subcomponents](https://dagger.dev/dev-guide/subcomponents). For example: @@ -235,7 +246,7 @@ Implementing `DatabaseMultiDb`: @Singleton class DatabaseMultiDb @Inject constructor(private val factory: DbInstanceComponent.Factory) { private val instances = mutableMapOf() - + @Synchronized fun get(dbName: String) : FirebaseDatabase { if (!instances.containsKey(dbName)) { diff --git a/contributor-docs/components/components.md b/contributor-docs/components/components.md index de624274aab..816d71c7842 100644 --- a/contributor-docs/components/components.md +++ b/contributor-docs/components/components.md @@ -5,21 +5,22 @@ nav_order: 4 --- # Firebase Components + {: .no_toc} -1. TOC -{: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 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 +- 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. +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. @@ -27,17 +28,19 @@ This page describes the aforementioned Component Model, how it works and why it' ### 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. +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 +- 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 @@ -47,11 +50,12 @@ The runtime does as little work as possible during initialization. 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.) +- 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: @@ -66,11 +70,11 @@ Component auth = Component.builder(FirebaseAuth.class, InternalAut .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): +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. @@ -80,18 +84,20 @@ interface RtdbComponent { } ``` -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. +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. +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: +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 { @@ -108,7 +114,8 @@ public class MyRegistrar implements ComponentRegistrar { ## Component Discovery -In addition to creating the `ComponentRegistrar` class, SDKs also need to add them to their `AndroidManifest.xml` under `ComponentDiscoveryService`: +In addition to creating the `ComponentRegistrar` class, SDKs also need to add them to their +`AndroidManifest.xml` under `ComponentDiscoveryService`: ```xml @@ -123,28 +130,32 @@ In addition to creating the `ComponentRegistrar` class, SDKs also need to add th ``` -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. +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. +- **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. +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 @@ -172,12 +183,12 @@ flowchart TD 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 @@ -186,33 +197,37 @@ flowchart TD 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. +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. +_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`. +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 +- 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 +- 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). +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 @@ -236,7 +251,7 @@ class FirebaseDatabase( companion object { fun getInstance(app : FirebaseApp) = getInstance("default") - fun getInstance(app : FirebaseApp, dbName: String) = + 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 index 587fe109ab1..b77fb34ab64 100644 --- a/contributor-docs/components/dependencies.md +++ b/contributor-docs/components/dependencies.md @@ -3,18 +3,18 @@ parent: Firebase Components --- # Dependencies + {: .no_toc} -1. TOC -{: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. +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. @@ -34,15 +34,14 @@ class MyComponent(private val dep : MyDep) { } ``` -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. +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` +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(); } @@ -58,19 +57,22 @@ class MyComponent(private val dep : Provider) { } ``` -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. +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. +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 %}). +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`: @@ -85,9 +87,9 @@ To declare a required dependency use one of the following in your `ComponentRegi ## 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. +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`: @@ -99,24 +101,26 @@ To declare an optional dependency use the following in your `ComponentRegistrar` 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. +{: .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 +{: .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. +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: +To solve this problem, Components have a notion of a `Deferred` dependency. A deferred is defined as +follows: ```java public interface Deferred { @@ -145,7 +149,8 @@ See [Dynamic Module Support]({{ site.baseurl }}{% link components/dynamic_module ## 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`: +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 @@ -157,21 +162,22 @@ Component.intoSetBuilder(SomeType.class) .build(); ``` -With the above setup each SDK contributes a value of `SomeType` into a `Set` which becomes available as a -`Set` dependency. +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: +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. +- `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 +{: .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. +`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: diff --git a/contributor-docs/components/executors.md b/contributor-docs/components/executors.md index f7fdc7c3a7b..f8aea35a476 100644 --- a/contributor-docs/components/executors.md +++ b/contributor-docs/components/executors.md @@ -3,20 +3,22 @@ parent: Firebase Components --- # Executors + {: .no_toc} -1. TOC -{: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/) +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. +These executors are available as components and can be requested by product SDKs as component +dependencies. Example: @@ -25,7 +27,7 @@ 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)) @@ -38,17 +40,17 @@ public class MyRegistrar implements ComponentRegistrar { All executors(with the exception of `@UiThread`) are available as the following interfaces: -* `Executor` -* `ExecutorService` -* `ScheduledExecutorService` -* `CoroutineDispatcher` +- `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. +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 @@ -65,17 +67,17 @@ flowchart TD 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; @@ -85,7 +87,8 @@ flowchart TD ### UiThread -Used to schedule tasks on application's UI thread, internally it uses a Handler to post runnables onto the main looper. +Used to schedule tasks on application's UI thread, internally it uses a Handler to post runnables +onto the main looper. Example: @@ -101,8 +104,8 @@ Qualified dispatcher = qualified(UiThread::class.java, Coro ### 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) +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: @@ -118,8 +121,8 @@ Qualified dispatcher = qualified(Lightweight::class.java, C ### Background -Use for tasks that may block on disk IO(use `@Blocking` for network IO or blocking on other threads). -Backed by 4 threads. +Use for tasks that may block on disk IO(use `@Blocking` for network IO or blocking on other +threads). Backed by 4 threads. Example: @@ -153,8 +156,8 @@ Qualified dispatcher = qualified(Blocking::class.java, Coro #### Direct executor -{: .warning } -Prefer `@Lightweight` instead of using direct executor as it could cause dead locks and stack overflows. +{: .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 @@ -166,7 +169,9 @@ 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()`. +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: @@ -179,13 +184,13 @@ Executor sequentialExecutor = FirebaseExecutors.newSequentialExecutor(c.get(bgEx ## Proper Kotlin usage -A `CoroutineContext` should be preferred when possible over an explicit `Executor` -or `CoroutineDispatcher`. You should only use an `Executor` at the highest -(or inversely the lowest) level of your implementations. Most classes should not -be concerned with the existence of an `Executor`. +A `CoroutineContext` should be preferred when possible over an explicit `Executor` or +`CoroutineDispatcher`. You should only use an `Executor` at the highest (or inversely the lowest) +level of your implementations. Most classes should not be concerned with the existence of an +`Executor`. -Keep in mind that you can combine `CoroutineContext` with other `CoroutineScope` -or `CoroutineContext`. And that all `suspend` functions inherent their `coroutineContext`: +Keep in mind that you can combine `CoroutineContext` with other `CoroutineScope` or +`CoroutineContext`. And that all `suspend` functions inherent their `coroutineContext`: ```kotlin suspend fun createSession(): Session { @@ -202,20 +207,19 @@ To learn more, you should give the following Kotlin wiki page a read: ### Using Executors in tests -`@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 +`@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). +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: +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 { @@ -237,35 +241,34 @@ TestOnlyExecutors.lite(); ### Policy violations in tests -Unit tests require [Robolectric](https://github.com/robolectric/robolectric) to -function correctly, and this comes with a major drawback; no policy validation. +Unit tests require [Robolectric](https://github.com/robolectric/robolectric) to function correctly, +and this comes with a major drawback; no policy validation. -Robolectric supports `StrictMode`- but does not provide the backing for its -policy mechanisms to fire on violations. As such, you'll be able to do things -like using `TestOnlyExecutors.background()` to execute blocking actions; usage -that would have otherwise crashed in a real application. +Robolectric supports `StrictMode`- but does not provide the backing for its policy mechanisms to +fire on violations. As such, you'll be able to do things like using `TestOnlyExecutors.background()` +to execute blocking actions; usage that would have otherwise crashed in a real application. -Unfortunately, there is no easy way to fix this for unit tests. You can get -around the issue by moving the tests to an emulator (integration tests)- but -those can be more expensive than your standard unit test, so you may want to -take that into consideration when planning your testing strategy. +Unfortunately, there is no easy way to fix this for unit tests. You can get around the issue by +moving the tests to an emulator (integration tests)- but those can be more expensive than your +standard unit test, so you may want to take that into consideration when planning your testing +strategy. ### StandardTestDispatcher support The [kotlin.coroutines.test](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/) -library provides support for a number of different mechanisms in tests. Some of the more -famous features include: +library provides support for a number of different mechanisms in tests. Some of the more famous +features include: - [advanceUntilIdle](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/advance-until-idle.html) - [advanceTimeBy](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/advance-time-by.html) - [runCurrent](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scheduler/run-current.html) -These features are all backed by `StandardTestDispatcher`, or more appropriately, -the `TestScope` provided in a `runTest` block. +These features are all backed by `StandardTestDispatcher`, or more appropriately, the `TestScope` +provided in a `runTest` block. -Unfortunately, `TestOnlyExecutors` does not natively bind with `TestScope`. -Meaning, should you use `TestOnlyExecutors` in your tests- you won't be able to utilize -the features provided by `TestScope`: +Unfortunately, `TestOnlyExecutors` does not natively bind with `TestScope`. Meaning, should you use +`TestOnlyExecutors` in your tests- you won't be able to utilize the features provided by +`TestScope`: ```kotlin @Test @@ -279,9 +282,8 @@ fun doesStuff() = runTest { } ``` -To help fix this, we provide an extension method on `TestScope` called -`firebaseExecutors`. It facilitates the binding of `TestOnlyExecutors` with the -current `TestScope`. +To help fix this, we provide an extension method on `TestScope` called `firebaseExecutors`. It +facilitates the binding of `TestOnlyExecutors` with the current `TestScope`. For example, here's how you could use this extension method in a test: @@ -295,4 +297,4 @@ fun doesStuff() = runTest { runCurrent() } -``` \ No newline at end of file +``` diff --git a/contributor-docs/how_firebase_works.md b/contributor-docs/how_firebase_works.md index 3d20eeb2374..bbfba97012e 100644 --- a/contributor-docs/how_firebase_works.md +++ b/contributor-docs/how_firebase_works.md @@ -8,54 +8,74 @@ nav_order: 3 ### 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: +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 +- `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. +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.: +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 +- `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. +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. +`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`. +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. +`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: +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 +- `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. +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/main/firebase-components). +To enable this functionality, Firebase uses a runtime discovery and dependency injection framework +[firebase-components](https://github.com/firebase/firebase-android-sdk/tree/main/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. +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 { @@ -80,6 +100,7 @@ public class MyRegistrar implements ComponentRegistrar { } ``` -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. +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/env_setup.md b/contributor-docs/onboarding/env_setup.md index 95427f8a66a..871cb910293 100644 --- a/contributor-docs/onboarding/env_setup.md +++ b/contributor-docs/onboarding/env_setup.md @@ -5,36 +5,33 @@ 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. +[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. +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`. +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) +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. +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). +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 +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 @@ -42,9 +39,9 @@ After that configure github keys as usual using this ## Importing the repository -1. Clone the repository with `git clone --recurse-submodules - git@github.com:firebase/firebase-android-sdk.git`. +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) + ![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 index 2d39b001d62..6dd619bb6e8 100644 --- a/contributor-docs/onboarding/new_sdk.md +++ b/contributor-docs/onboarding/new_sdk.md @@ -3,23 +3,22 @@ parent: Onboarding --- # Creating a new Firebase SDK + {: .no_toc} -1. TOC -{:toc} +1. TOC {:toc} Want to create a new SDK in -[firebase/firebase-android-sdk](https://github.com/firebase/firebase-android-sdk)? -Read on. +[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). +[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 @@ -35,21 +34,17 @@ firebase-android-sdk └── 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. +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. +{: .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 `plugins` and is used by all -SDKs. +All of the core Gradle build logic lives in `plugins` and is used by all SDKs. -SDKs can be grouped together for convenience by placing them in a directory of -choice. +SDKs can be grouped together for convenience by placing them in a directory of choice. ## Creating an SDK @@ -72,34 +67,21 @@ plugins { // id("kotlin-android") } -firebaseLibrary { - // enable this only if you have tests in `androidTest`. - testLab.enabled = true - publishJavadoc = true -} +firebaseLibrary { // enable this only if you have tests in `androidTest`. testLab.enabled = 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" - } +android { val targetSdkVersion : Int by rootProject val minSdkVersion : Int by rootProject - testOptions.unitTests.isIncludeAndroidResources = true -} +compileSdk = targetSdkVersion defaultConfig { namespace = "com.google.firebase.foo" // change this +if you have custom needs. minSdk = minSdkVersion targetSdk = targetSdkVersion +testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } -dependencies { - implementation("com.google.firebase:firebase-common:21.0.0") - implementation("com.google.firebase:firebase-components:18.0.0") -} +testOptions.unitTests.isIncludeAndroidResources = true } -``` +dependencies { implementation("com.google.firebase:firebase-common:21.0.0") +implementation("com.google.firebase:firebase-components:18.0.0") } + +```` ### Create `src/main/AndroidManifest.xml` with the following content: @@ -134,13 +116,14 @@ dependencies { -``` +```` ### Create `com.google.firebase.foo.FirebaseFoo` For Kotlin +
src/main/kotlin/com/google/firebase/foo/FirebaseFoo.kt @@ -161,6 +144,7 @@ class FirebaseFoo {
For Java +
src/main/java/com/google/firebase/foo/FirebaseFoo.java @@ -182,14 +166,15 @@ public class FirebaseFoo { ### 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. +{: .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 { @@ -204,6 +189,7 @@ class FirebaseFooRegistrar : ComponentRegistrar {
For Java +
src/main/java/com/google/firebase/foo/FirebaseFooRegistrar.java diff --git a/docs/README.md b/docs/README.md index 7d764ead5f2..dfedd7abcf4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,39 +1,37 @@ # Firebase Android SDK -The Firebase SDK for Android is the official way to add Firebase to your -Android app. To get started, visit the [setup instructions][android-setup]. +The Firebase SDK for Android is the official way to add Firebase to your Android app. To get +started, visit the [setup instructions][android-setup]. ## Open Source This repository includes the following Firebase SDKs for Android: - * `firebase-common` - * `firebase-database` - * `firebase-firestore` - * `firebase-functions` - * `firebase-inappmessaging-display` - * `firebase-perf` - * `firebase-storage` +- `firebase-common` +- `firebase-database` +- `firebase-firestore` +- `firebase-functions` +- `firebase-inappmessaging-display` +- `firebase-perf` +- `firebase-storage` -For more information on building the SDKs from source or contributing, -visit the [main README][main-readme]. +For more information on building the SDKs from source or contributing, visit the [main +README][main-readme]. ## Kotlin Extensions -The following Firebase SDKs for Android have Kotlin extension libraries -that allow you to write more idiomatic Kotlin code when using Firebase -in your app: - - * [`firebase-common`](ktx/common.md) - * [`firebase-crashlytics`](ktx/crashlytics.md) - * [`firebase-dynamic-links`](ktx/dynamic-links.md) - * [`firebase-firestore`](ktx/firestore.md) - * [`firebase-functions`](ktx/functions.md) - * [`firebase-inappmessaging`](ktx/inappmessaging.md) - * [`firebase-inappmessaging-display`](ktx/inappmessaging-display.md) - * [`firebase-remote-config`](ktx/remote-config.md) - * [`firebase-storage`](ktx/storage.md) - * [`firebase-database`](ktx/database.md) +The following Firebase SDKs for Android have Kotlin extension libraries that allow you to write more +idiomatic Kotlin code when using Firebase in your app: + +- [`firebase-common`](ktx/common.md) +- [`firebase-crashlytics`](ktx/crashlytics.md) +- [`firebase-firestore`](ktx/firestore.md) +- [`firebase-functions`](ktx/functions.md) +- [`firebase-inappmessaging`](ktx/inappmessaging.md) +- [`firebase-inappmessaging-display`](ktx/inappmessaging-display.md) +- [`firebase-remote-config`](ktx/remote-config.md) +- [`firebase-storage`](ktx/storage.md) +- [`firebase-database`](ktx/database.md) [android-setup]: https://firebase.google.com/docs/android/setup [main-readme]: https://github.com/firebase/firebase-android-sdk/blob/main/README.md diff --git a/docs/ktx/common.md b/docs/ktx/common.md deleted file mode 100644 index 3935c9e1b9c..00000000000 --- a/docs/ktx/common.md +++ /dev/null @@ -1,42 +0,0 @@ -# Firebase Common Kotlin Extensions - -## Getting Started - -To use the Firebase Common Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-common library -implementation 'com.google.firebase:firebase-common-ktx:$VERSION' -``` - -## Features - -### Get the default FirebaseApp and FirebaseOptions - -**Kotlin** -```kotlin -val defaultApp = FirebaseApp.getInstance() -val defaultOptions = defaultApp.options -``` - -**Kotlin + KTX** -```kotlin -val defaultApp = Firebase.app -val defaultOptions = Firebase.options -``` - -### Initialize a FirebaseApp - -**Kotlin** -```kotlin -val options = FirebaseApp.getInstance().options -val anotherApp = FirebaseApp.initializeApp(context, options, "myApp") -``` - -**Kotlin + KTX** -```kotlin -var anotherApp = Firebase.initialize(context, Firebase.options, "myApp") -``` - diff --git a/docs/ktx/crashlytics.md b/docs/ktx/crashlytics.md deleted file mode 100644 index 687944ba2e4..00000000000 --- a/docs/ktx/crashlytics.md +++ /dev/null @@ -1,50 +0,0 @@ -# Crashlytics Kotlin Extensions - -## Getting Started - -To use the Firebase Crashlytics Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-crashlytics library -implementation 'com.google.firebase:firebase-crashlytics-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseCrashlytics - -**Kotlin** -```kotlin -val crashlytics = FirebaseCrashlytics.getInstance() -``` - -**Kotlin + KTX** -```kotlin -val crashlytics = Firebase.crashlytics -``` - -### Set custom keys - -**Kotlin** -```kotlin -crashlytics.setCustomKey("str_key", "hello") -crashlytics.setCustomKey("bool_key", true) -crashlytics.setCustomKey("int_key", 1) -crashlytics.setCustomKey("long_key", 1L) -crashlytics.setCustomKey("float_key", 1.0f) -crashlytics.setCustomKey("double_key", 1.0) -``` - -**Kotlin + KTX** -```kotlin -crashlytics.setCustomKeys { - key("str_key", "hello") - key("bool_key", true) - key("int_key", 1) - key("long_key", 1L) - key("float_key", 1.0f) - key("double_key", 1.0) -} -``` diff --git a/docs/ktx/database.md b/docs/ktx/database.md deleted file mode 100644 index d54d3497c84..00000000000 --- a/docs/ktx/database.md +++ /dev/null @@ -1,100 +0,0 @@ -# Realtime Database Kotlin Extensions - -## Getting Started - -To use the Firebase Realtime Database Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-database library -implementation 'com.google.firebase:firebase-database-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseDatabase - -**Kotlin** -```kotlin -val database = FirebaseDatabase.getInstance() -val anotherDatabase = FirebaseDatabase.getInstance(FirebaseApp.getInstance("myApp")) -``` - -**Kotlin + KTX** -```kotlin -val database = Firebase.database -val anotherDatabase = Firebase.database(Firebase.app("myApp")) -``` - -### Get the FirebaseDatabase for the specified url - -**Kotlin** -```kotlin -val database = FirebaseDatabase.getInstance(url) -``` - -**Kotlin + KTX** -```kotlin -val database = Firebase.database(url) -``` - - -### Get the FirebaseDatabase of the given FirebaseApp and url - -**Kotlin** -```kotlin -val database = FirebaseDatabase.getInstance(app, url) -``` - -**Kotlin + KTX** -```kotlin -val database = Firebase.database(app, url) -``` - -### Convert a DataSnapshot to a POJO - -**Kotlin** -```kotlin -val snapshot: DataSnapshot = ... -val myObject = snapshot.getValue(MyClass::class.java) -``` - -**Kotlin + KTX** -```kotlin -val snapshot: DocumentSnapshot = ... -val myObject = snapshot.getValue() -``` - -### Convert a DataSnapshot to generic types such as List or Map - -**Kotlin** -```kotlin -val snapshot: DataSnapshot = ... -val typeIndicator = object : GenericTypeIndicator>() {} -val messages: List = snapshot.getValue(typeIndicator) -``` - -**Kotlin + KTX** -```kotlin -val snapshot: DocumentSnapshot = ... -val messages: List = snapshot.getValue>() -``` - -### Convert a MutableData to a POJO in a Transaction - -**Kotlin** -```kotlin -override fun doTransaction(mutableData: MutableData): Transaction.Result { - val post = mutableData.getValue(Post::class.java) - // ... -} -``` - -**Kotlin + KTX** -```kotlin -override fun doTransaction(mutableData: MutableData): Transaction.Result { - val post = mutableData.getValue() - // ... -} -``` diff --git a/docs/ktx/dynamic-links.md b/docs/ktx/dynamic-links.md deleted file mode 100644 index 187dade8b41..00000000000 --- a/docs/ktx/dynamic-links.md +++ /dev/null @@ -1,170 +0,0 @@ -# Dynamic Links Kotlin Extensions - -## Getting Started - -To use the Dynamic Links Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-dynamic-links library -implementation 'com.google.firebase:firebase-dynamic-links-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseDynamicLinks - -**Kotlin** -```kotlin -val dynamicLinks = FirebaseDynamicLinks.getInstance() -val anotherDynamicLinks = FirebaseDynamicLinks.getInstance(FirebaseApp.getInstance("myApp")) -``` - -**Kotlin + KTX** -```kotlin -val dynamicLinks = Firebase.dynamicLinks -val anotherDynamicLinks = Firebase.dynamicLinks(Firebase.app("myApp")) -``` - -### Create a Dynamic Link from parameters - -**Kotlin** -```kotlin -val dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink() - .setLink(Uri.parse("https://www.example.com/")) - .setDomainUriPrefix("https://example.page.link") - .setAndroidParameters( - DynamicLink.AndroidParameters.Builder("com.example.android") - .setMinimumVersion(16) - .build()) - .setIosParameters( - DynamicLink.IosParameters.Builder("com.example.ios") - .setAppStoreId("123456789") - .setMinimumVersion("1.0.1") - .build()) - .setGoogleAnalyticsParameters( - DynamicLink.GoogleAnalyticsParameters.Builder() - .setSource("orkut") - .setMedium("social") - .setCampaign("example-promo") - .build()) - .setItunesConnectAnalyticsParameters( - DynamicLink.ItunesConnectAnalyticsParameters.Builder() - .setProviderToken("123456") - .setCampaignToken("example-promo") - .build()) - .setSocialMetaTagParameters( - DynamicLink.SocialMetaTagParameters.Builder() - .setTitle("Example of a Dynamic Link") - .setDescription("This link works whether the app is installed or not!") - .build()) - .buildDynamicLink() -``` - -**Kotlin + KTX** -```kotlin -val dynamicLink = Firebase.dynamicLinks.dynamicLink { - link = Uri.parse("https://www.example.com/") - domainUriPrefix = "https://example.page.link" - androidParameters("com.example.android") { - minimumVersion = 16 - } - iosParameters("com.example.ios") { - appStoreId = "123456789" - minimumVersion = "1.0.1" - } - googleAnalyticsParameters { - source = "orkut" - medium = "social" - campaign = "example-promo" - } - itunesConnectAnalyticsParameters { - providerToken = "123456" - campaignToken = "example-promo" - } - socialMetaTagParameters { - title = "Example of a Dynamic Link" - description = "This link works whether the app is installed or not!" - } -} -``` - -### Shorten a long Dynamic Link - -**Kotlin** -```kotlin -FirebaseDynamicLinks.getInstance().createDynamicLink() - .setLongLink(Uri.parse("https://example.page.link/?link=" + - "https://www.example.com/&apn=com.example.android&ibn=com.example.ios")) - .buildShortDynamicLink() - .addOnSuccessListener { result -> - // Short link created - val shortLink = result.shortLink - val flowchartLink = result.previewLink - val warnings = result.warnings - - // do something with the links and warnings - showLinks(shortLink, flowchartLink) - displayWarnings(warnings) - } - .addOnFailureListener { - // Error - // ... - } -``` - -**Kotlin + KTX** -```kotlin -Firebase.dynamicLinks.shortLinkAsync { - longLink = Uri.parse("https://example.page.link/?link=" + - "https://www.example.com/&apn=com.example.android&ibn=com.example.ios") -}.addOnSuccessListener { (shortLink, flowchartLink, warnings) -> - // do something with the links and warnings - showLinks(shortLink, flowchartLink) - displayWarnings(warnings) -}.addOnFailureListener { - // Error - // ... -} -``` - -### Create a Dynamic Link with a shorter link suffix - -**Kotlin** -```kotlin -val shortLinkTask = FirebaseDynamicLinks.getInstance().createDynamicLink() - // ... - .buildShortDynamicLink(ShortDynamicLink.Suffix.SHORT) -``` - -**Kotlin + KTX** -```kotlin -val shortLinkTask = Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) { - // ... -} -``` - -### Receive deep links - -**Kotlin** -```kotlin -Firebase.dynamicLinks - .getDynamicLink(intent) - .addOnSuccessListener(this) { pendingDynamicLinkData -> - val deepLink = pendingDynamicLinkData.link - val minAppVersion = pendingDynamicLinkData.minimumAppVersion - val clickTimestamp = pendingDynamicLinkData.clickTimestamp - - // TODO(developer): handle the deepLink - }.addOnFailureListener { /* ... */ } -``` - -**Kotlin + KTX** -```kotlin -Firebase.dynamicLinks - .getDynamicLink(intent) - .addOnSuccessListener(this) { (deepLink, minAppVersion, clickTimestamp) -> - // TODO(developer): handle the deepLink - }.addOnFailureListener { /* ... */ } -``` diff --git a/docs/ktx/firestore.md b/docs/ktx/firestore.md deleted file mode 100644 index 276f6bc0820..00000000000 --- a/docs/ktx/firestore.md +++ /dev/null @@ -1,150 +0,0 @@ -# Firestore Kotlin Extensions - -## Getting Started - -To use the Cloud Firestore Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-firestore library -implementation 'com.google.firebase:firebase-firestore-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseFirestore - -**Kotlin** -```kotlin -val firestore = FirebaseFirestore.getInstance() -val anotherFirestore = FirebaseFirestore.getInstance(FirebaseApp.getInstance("myApp")) -``` - -**Kotlin + KTX** -```kotlin -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** -```kotlin -val snapshot: DocumentSnapshot = ... -val myObject = snapshot.get("fieldPath", MyClass::class.java) -``` - -**Kotlin + KTX** -```kotlin -val snapshot: DocumentSnapshot = ... -val myObject = snapshot.get("fieldPath") -``` - -### Convert a DocumentSnapshot to a POJO - -**Kotlin** -```kotlin -val snapshot: DocumentSnapshot = ... -val myObject = snapshot.toObject(MyClass::class.java) -``` - -**Kotlin + KTX** -```kotlin -val snapshot: DocumentSnapshot = ... -val myObject = snapshot.toObject() -``` - -### Convert a QuerySnapshot to a list of POJOs - -**Kotlin** -```kotlin -val snapshot: QuerySnapshot = ... -val objectList = snapshot.toObjects(MyClass::class.java) -``` - -**Kotlin + KTX** -```kotlin -val snapshot: QuerySnapshot = ... -val objectList = snapshot.toObjects() -``` - -### Setup Firestore with a local emulator - -**Kotlin** -```kotlin -val settings = FirebaseFirestoreSettings.Builder() - .setHost("10.0.2.2:8080") - .setSslEnabled(false) - .setPersistenceEnabled(false) - .build() - -firestore.setFirestoreSettings(settings) -``` - -**Kotlin + KTX** -```kotlin -firestore.firestoreSettings = firestoreSettings { - host = "http://10.0.2.2:8080" - isSslEnabled = false - isPersistenceEnabled = false -} -``` diff --git a/docs/ktx/functions.md b/docs/ktx/functions.md deleted file mode 100644 index 6de19ce53ed..00000000000 --- a/docs/ktx/functions.md +++ /dev/null @@ -1,62 +0,0 @@ -# Cloud Functions Kotlin Extensions - -## Getting Started - -To use the Cloud Functions Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-functions library -implementation 'com.google.firebase:firebase-functions-ktx:$VERSION' -``` - -## Features - -### Get the FirebaseFunctions instance of the default app - -**Kotlin** -```kotlin -val functions = FirebaseFunctions.getInstance() -``` - -**Kotlin + KTX** -```kotlin -val functions = Firebase.functions -``` - -### Get the FirebaseFunctions of a given region - -**Kotlin** -```kotlin -val functions = FirebaseFunctions.getInstance(region) -``` - -**Kotlin + KTX** -```kotlin -val functions = Firebase.functions(region) -``` - -### Get the FirebaseFunctions of a given FirebaseApp - -**Kotlin** -```kotlin -val functions = FirebaseFunctions.getInstance(app) -``` - -**Kotlin + KTX** -```kotlin -val functions = Firebase.functions(app) -``` - -### Get the FirebaseFunctions of a given region and FirebaseApp - -**Kotlin** -```kotlin -val functions = FirebaseFunctions.getInstance(app, region) -``` - -**Kotlin + KTX** -```kotlin -val functions = Firebase.functions(app, region) -``` diff --git a/docs/ktx/inappmessaging-display.md b/docs/ktx/inappmessaging-display.md deleted file mode 100644 index 82a6d3702cc..00000000000 --- a/docs/ktx/inappmessaging-display.md +++ /dev/null @@ -1,26 +0,0 @@ -# In-App Messaging Display Kotlin Extensions - -## Getting Started - -To use the Firebase In-App Messaging Display Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-inappmessaging-display library -implementation 'com.google.firebase:firebase-inappmessaging-display-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseInAppMessagingDisplay - -**Kotlin** -```kotlin -val fiamUI = FirebaseInAppMessagingDisplay.getInstance() -``` - -**Kotlin + KTX** -```kotlin -val fiamUI = Firebase.inAppMessagingDisplay -``` diff --git a/docs/ktx/inappmessaging.md b/docs/ktx/inappmessaging.md deleted file mode 100644 index 13f90d7d84f..00000000000 --- a/docs/ktx/inappmessaging.md +++ /dev/null @@ -1,26 +0,0 @@ -# In-App Messaging Kotlin Extensions - -## Getting Started - -To use the Firebase In-App Messaging Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-inappmessaging library -implementation 'com.google.firebase:firebase-inappmessaging-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseInAppMessaging - -**Kotlin** -```kotlin -val fiamUI = FirebaseInAppMessaging.getInstance() -``` - -**Kotlin + KTX** -```kotlin -val fiamUI = Firebase.inAppMessaging -``` diff --git a/docs/ktx/remote-config.md b/docs/ktx/remote-config.md deleted file mode 100644 index 4f685cea793..00000000000 --- a/docs/ktx/remote-config.md +++ /dev/null @@ -1,86 +0,0 @@ -# Remote Config Kotlin Extensions - -## Getting Started - -To use the Firebase Remote Config Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-config library -implementation 'com.google.firebase:firebase-config-ktx:$VERSION' -``` - -## Features - -### Get the FirebaseRemoteConfig instance of the default app - -**Kotlin** -```kotlin -val remoteConfig = FirebaseRemoteConfig.getInstance() -``` - -**Kotlin + KTX** -```kotlin -val remoteConfig = Firebase.remoteConfig -``` - -### Get the FirebaseRemoteConfig of a given FirebaseApp - -**Kotlin** -```kotlin -val remoteConfig = FirebaseRemoteConfig.getInstance(app) -``` - -**Kotlin + KTX** -```kotlin -val remoteConfig = Firebase.remoteConfig(app) -``` - -### Get parameter values from FirebaseRemoteConfig - -**Kotlin** -```kotlin -val isEnabled = remoteConfig.getBoolean("is_enabled") - -val fileBytes = remoteConfig.getByteArray("file_bytes") - -val audioVolume = remoteConfig.getDouble("audio_volume") - -val maxCharacters = remoteConfig.getLong("max_characters") - -val accessKey = remoteConfig.getString("access_key") -``` - -**Kotlin + KTX** -```kotlin -val isEnabled = remoteConfig["is_enabled"].asBoolean() - -val fileBytes = remoteConfig["file_bytes"].asByteArray() - -val audioVolume = remoteConfig["audio_volume"].asDouble() - -val maxCharacters = remoteConfig["max_characters"].asLong() - -val accessKey = remoteConfig["access_key"].asString() -``` - -### Set Remote Config Settings - -**Kotlin** -```kotlin -val configSettings = FirebaseRemoteConfigSettings.Builder() - .setMinimumFetchIntervalInSeconds(3600) - .setFetchTimeoutInSeconds(60) - .build() -remoteConfig.setConfigSettingsAsync(configSettings) -``` - -**Kotlin + KTX** -```kotlin -val configSettings = remoteConfigSettings { - minimumFetchIntervalInSeconds = 3600 - fetchTimeoutInSeconds = 60 -} -remoteConfig.setConfigSettingsAsync(configSettings) -``` \ No newline at end of file diff --git a/docs/ktx/storage.md b/docs/ktx/storage.md deleted file mode 100644 index 5854723fe6e..00000000000 --- a/docs/ktx/storage.md +++ /dev/null @@ -1,62 +0,0 @@ -# Storage Kotlin Extensions - -## Getting Started - -To use the Cloud Storage Android SDK with Kotlin Extensions, add the following -to your app's `build.gradle` file: - -```groovy -// See maven.google.com for the latest versions -// This library transitively includes the firebase-storage library -implementation 'com.google.firebase:firebase-storage-ktx:$VERSION' -``` - -## Features - -### Get an instance of FirebaseStorage - -**Kotlin** -```kotlin -val storage = FirebaseStorage.getInstance() -val anotherStorage = FirebaseStorage.getInstance(FirebaseApp.getInstance("myApp")) -``` - -**Kotlin + KTX** -```kotlin -val storage = Firebase.storage -val anotherStorage = Firebase.storage(Firebase.app("myApp")) -``` - -### Get the FirebaseStorage for a custom storage bucket url - -**Kotlin** -```kotlin -val storage = FirebaseStorage.getInstance("gs://my-custom-bucket") -val anotherStorage = FirebaseStorage.getInstance(FirebaseApp.getInstance("myApp"), "gs://my-custom-bucket") -``` - -**Kotlin + KTX** -```kotlin -val storage = Firebase.storage("gs://my-custom-bucket") -val anotherStorage = Firebase.storage(Firebase.app("myApp"), "gs://my-custom-bucket") -``` - -### Create file metadata - -**Kotlin** -```kotlin -val metadata = StorageMetadata.Builder() - .setContentType("image/jpg") - .setContentDisposition("attachment") - .setCustomMetadata("location", "Maputo, MOZ") - .build() -``` - -**Kotlin + KTX** -```kotlin -val metadata = storageMetadata { - contentType = "image/jpg" - contentDisposition = "attachment" - setCustomMetadata("location", "Maputo, MOZ") -} -``` diff --git a/encoders/README.md b/encoders/README.md index 6a33fcb1c81..afc897087d2 100644 --- a/encoders/README.md +++ b/encoders/README.md @@ -1,27 +1,24 @@ # Firebase Encoders -This project provides libraries and code generation infrastructure that allows -encoding java classes into various target serialization formats(currently -supported: **json** and **proto**). +This project provides libraries and code generation infrastructure that allows encoding java classes +into various target serialization formats(currently supported: **json** and **proto**). The project consists of multiple parts: -* `firebase_encoders` - Core API and Annotations library. -* `processor` - Java plugin that automatically generates encoders for - `@Encodable` annotated POJOs. -* `firebase_encoders_json` - JSON serialization support. -* `firebase_encoders_proto` - Protobuf serialization support. -* `protoc_gen` - Protobuf compiler plugin that generates encoder-compliant - classes. Can be used with `firebase_encoders_proto` and - `firebase_encoders_json`. -* `reflective` - Can be used to encode any given class via Java - reflection(**not recommented**). +- `firebase_encoders` - Core API and Annotations library. +- `processor` - Java plugin that automatically generates encoders for `@Encodable` annotated POJOs. +- `firebase_encoders_json` - JSON serialization support. +- `firebase_encoders_proto` - Protobuf serialization support. +- `protoc_gen` - Protobuf compiler plugin that generates encoder-compliant classes. Can be used with + `firebase_encoders_proto` and `firebase_encoders_json`. +- `reflective` - Can be used to encode any given class via Java reflection(**not recommented**). ### Protobuf gettings started ##### Step1. Place proto files into **src/main/proto/** -*src/main/proto/my.proto* +_src/main/proto/my.proto_ + ```proto syntax = "proto3"; @@ -35,10 +32,10 @@ message SimpleProto { } ``` - ##### Step2. Add the following configurations into gradle module build file. -*example.gradle* +_example.gradle_ + ```gradle plugins { id "java-library" @@ -86,11 +83,14 @@ dependencies { ##### Step3. Create a code-gen-cfg.textproto file at the module root folder(same location as the gradle module build file). -*code-gen-cfg.textproto* +_code-gen-cfg.textproto_ Note: + - The filename must be the same as the filename determined in the gradle build file. -- Only need to specify the "root" proto object, anything it references will automatically be included. +- Only need to specify the "root" proto object, anything it references will automatically be + included. + ```textproto # code_gen_cfg.textproto # proto-file: src/main/proto/my.proto @@ -112,8 +112,7 @@ com.google.google.protobuf.Timestamp com.google.google.protobuf.Timestamp$Builder ``` -Only `root` classes are "encodable" meaning that they have the following -methods: +Only `root` classes are "encodable" meaning that they have the following methods: ```java public class SimpleProto { @@ -124,7 +123,8 @@ public class SimpleProto { ### Annotation Processing on Kotlin -The default gradle `annotationProcessor` import doesn't run the processor over kotlin code, so we need to use `kapt` +The default gradle `annotationProcessor` import doesn't run the processor over kotlin code, so we +need to use `kapt` 1. Add the plugin to your build @@ -145,4 +145,4 @@ dependencies { // annotationProcessor project(":encoders:firebase-encoders-processor") kapt project(":encoders:firebase-encoders-processor") } -``` \ No newline at end of file +``` diff --git a/encoders/firebase-decoders-json/CHANGELOG.md b/encoders/firebase-decoders-json/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/firebase-decoders-json/CHANGELOG.md +++ b/encoders/firebase-decoders-json/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/encoders/firebase-encoders-json/CHANGELOG.md b/encoders/firebase-encoders-json/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/firebase-encoders-json/CHANGELOG.md +++ b/encoders/firebase-encoders-json/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/encoders/firebase-encoders-json/firebase-encoders-json.gradle b/encoders/firebase-encoders-json/firebase-encoders-json.gradle index d054982b2dd..beb4cb70a4b 100644 --- a/encoders/firebase-encoders-json/firebase-encoders-json.gradle +++ b/encoders/firebase-encoders-json/firebase-encoders-json.gradle @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id 'firebase-library' id 'kotlin-android' @@ -19,7 +21,7 @@ plugins { firebaseLibrary { publishJavadoc = false - releaseNotes { + releaseNotes { enabled.set(false) } } @@ -38,9 +40,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } testOptions { unitTests { includeAndroidResources = true @@ -48,6 +47,8 @@ android { } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { api 'com.google.firebase:firebase-encoders:17.0.0' diff --git a/encoders/firebase-encoders-processor/CHANGELOG.md b/encoders/firebase-encoders-processor/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/firebase-encoders-processor/CHANGELOG.md +++ b/encoders/firebase-encoders-processor/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/encoders/firebase-encoders-proto/CHANGELOG.md b/encoders/firebase-encoders-proto/CHANGELOG.md index f4e346bb920..d43e9b27315 100644 --- a/encoders/firebase-encoders-proto/CHANGELOG.md +++ b/encoders/firebase-encoders-proto/CHANGELOG.md @@ -1,4 +1,4 @@ # Unreleased -* [changed] Updated protobuf dependency to `3.25.5` to fix - [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). +- [changed] Updated protobuf dependency to `3.25.5` to fix + [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/encoders/firebase-encoders-reflective/CHANGELOG.md b/encoders/firebase-encoders-reflective/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/firebase-encoders-reflective/CHANGELOG.md +++ b/encoders/firebase-encoders-reflective/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/encoders/firebase-encoders/CHANGELOG.md b/encoders/firebase-encoders/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/firebase-encoders/CHANGELOG.md +++ b/encoders/firebase-encoders/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/encoders/protoc-gen-firebase-encoders/CHANGELOG.md b/encoders/protoc-gen-firebase-encoders/CHANGELOG.md index f514bbb890e..79e701b844d 100644 --- a/encoders/protoc-gen-firebase-encoders/CHANGELOG.md +++ b/encoders/protoc-gen-firebase-encoders/CHANGELOG.md @@ -1,3 +1 @@ # Unreleased - - diff --git a/firebase-abt/CHANGELOG.md b/firebase-abt/CHANGELOG.md index d488ea79620..5d6730988ed 100644 --- a/firebase-abt/CHANGELOG.md +++ b/firebase-abt/CHANGELOG.md @@ -1,47 +1,64 @@ # Unreleased +# 23.0.1 + +- [changed] Bumped internal dependencies. + +# 23.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. +- [removed] **Breaking Change**: Stopped releasing the deprecated Kotlin extensions (KTX) module and + removed it from the Firebase Android BoM. Instead, use the KTX APIs from the main module. For + details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration). # 22.0.0 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 21.1.2 -* [changed] Bump internal dependencies. + +- [changed] Bump internal dependencies. # 21.1.1 -* [changed] Internal changes to improve experiment reporting. + +- [changed] Internal changes to improve experiment reporting. # 21.1.0 -* [changed] Internal changes to ensure functionality alignment with other - SDK releases. + +- [changed] Internal changes to ensure functionality alignment with other SDK releases. # 21.0.2 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). + +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). # 21.0.1 -* [changed] Updated dependencies of `play-services-basement`, - `play-services-base`, and `play-services-tasks` to their latest versions - (v18.0.0, v18.0.1, and v18.0.1, respectively). For more information, see the - [note](#basement18-0-0_base18-0-1_tasks18-0-1) at the top of this release - entry. + +- [changed] Updated dependencies of `play-services-basement`, `play-services-base`, and + `play-services-tasks` to their latest versions (v18.0.0, v18.0.1, and v18.0.1, respectively). For + more information, see the [note](#basement18-0-0_base18-0-1_tasks18-0-1) at the top of this + release entry. # 21.0.0 -* [changed] Internal infrastructure improvements. -* [changed] Internal changes to support dynamic feature modules. + +- [changed] Internal infrastructure improvements. +- [changed] Internal changes to support dynamic feature modules. # 20.0.0 -* [removed] Removed the protocol buffer dependency and moved relevant protocol - buffer definitions to [inappmessaging_longer]. If you use [ab_testing] - with [inappmessaging], you'll need to update to + +- [removed] Removed the protocol buffer dependency and moved relevant protocol buffer definitions to + [inappmessaging_longer]. If you use [ab_testing] with [inappmessaging], you'll need to update to [inappmessaging] v19.1.2 or later. # 19.0.1 -* [changed] Internal changes to ensure functionality alignment with other SDK releases. + +- [changed] Internal changes to ensure functionality alignment with other SDK releases. # 17.1.1 -* [changed] Updated API to support the latest [remote_config] update. -* [changed] Updated minSdkVersion to API level 16. + +- [changed] Updated API to support the latest [remote_config] update. +- [changed] Updated minSdkVersion to API level 16. # 17.1.0 -* [changed] Updated API to support the latest [remote_config] update. +- [changed] Updated API to support the latest [remote_config] update. diff --git a/firebase-abt/firebase-abt.gradle b/firebase-abt/firebase-abt.gradle index 3743c3b9546..3d63dfb2cb2 100644 --- a/firebase-abt/firebase-abt.gradle +++ b/firebase-abt/firebase-abt.gradle @@ -22,7 +22,6 @@ firebaseLibrary { releaseNotes { name.set("{{ab_testing}}") versionName.set("ab_testing") - hasKTX.set(false) } } @@ -52,8 +51,8 @@ android { } dependencies { - api 'com.google.firebase:firebase-common:21.0.0' - api 'com.google.firebase:firebase-components:18.0.0' + api libs.firebase.common + api libs.firebase.components implementation libs.playservices.basement implementation ('com.google.firebase:firebase-measurement-connector:18.0.0') { diff --git a/firebase-abt/gradle.properties b/firebase-abt/gradle.properties index 97257e8c980..250707abd72 100644 --- a/firebase-abt/gradle.properties +++ b/firebase-abt/gradle.properties @@ -1,2 +1,2 @@ -version=22.0.1 -latestReleasedVersion=22.0.0 +version=23.0.2 +latestReleasedVersion=23.0.1 diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index dd818c29cec..abf0bf55c68 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,28 +1,108 @@ # Unreleased -* [fixed] Fixed `FirebaseAI.getInstance` StackOverflowException (#6971) -* [fixed] Fixed an issue that was causing the SDK to send empty `FunctionDeclaration` descriptions to the API. -* [changed] Introduced the `Voice` class, which accepts a voice name, and deprecated the `Voices` class. -* [changed] **Breaking Change**: Updated `SpeechConfig` to take in `Voice` class instead of `Voices` class. - * **Action Required:** Update all references of `SpeechConfig` initialization to use `Voice` class. -* [fixed] Fix incorrect model name in count token requests to the developer API backend - - +- [changed] Added better scheduling and louder output for Live API. +- [changed] Added support for input and output transcription. (#7482) +- [feature] Added support for sending realtime audio and video in a `LiveSession`. +- [changed] Removed redundant internal exception types. (#7475) + +# 17.4.0 + +- [changed] **Breaking Change**: Removed the `candidateCount` option from `LiveGenerationConfig` +- [changed] Added support for user interrupts for the `startAudioConversation` method in the + `LiveSession` class. (#7413) +- [changed] Added support for the URL context tool, which allows the model to access content from + provided public web URLs to inform and enhance its responses. (#7382) +- [changed] Added better error messages to `ServiceConnectionHandshakeFailedException` (#7412) +- [changed] Marked the public constructor for `UsageMetadata` as deprecated (#7420) +- [changed] Using Firebase AI Logic with the Gemini Developer API is now Generally Available (GA). +- [changed] Using Firebase AI Logic with the Imagen generation APIs is now Generally Available (GA). + +# 17.3.0 + +- [changed] Bumped internal dependencies. +- [feature] Added support for code execution. +- [changed] Marked the public constructors for `ExecutableCodePart` and `CodeExecutionResultPart` as + deprecated. +- [feature] Introduced `MissingPermissionsException`, which is thrown when the necessary permissions + have not been granted by the user. +- [feature] Added helper functions to `LiveSession` to allow developers to track the status of the + audio session and the underlying websocket connection. +- [changed] Added new values to `HarmCategory` (#7324) +- [fixed] Fixed an issue that caused unknown or empty `Part`s to throw an exception. Instead, we now + log them and filter them from the response (#7333) + +# 17.2.0 + +- [feature] Added support for returning thought summaries, which are synthesized versions of a + model's internal reasoning process. +- [fixed] Fixed an issue causing the accessor methods in `GenerateContentResponse` to throw an + exception when the response contained no candidates. +- [changed] Added better description for requests which fail due to the Gemini API not being + configured. +- [changed] Added a `dilation` parameter to `ImagenMaskReference.generateMaskAndPadForOutpainting` + (#7260) +- [feature] Added support for limited-use tokens with Firebase App Check. These limited-use tokens + are required for an upcoming optional feature called _replay protection_. We recommend + [enabling the usage of limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check) + now so that when replay protection becomes available, you can enable it sooner because more of + your users will be on versions of your app that send limited-use tokens. (#7285) + +# 17.1.0 + +======= + +- [feature] added support for Imagen Editing, including inpainting, outpainting, control, style + transfer, and subject references (#7075) +- [feature] **Preview:** Added support for bidirectional streaming in Gemini Developer Api + +# 17.0.0 + +- [feature] Added support for configuring the "thinking" budget when using Gemini 2.5 series models. + (#6990) +- [feature] **Breaking Change**: Add support for grounding with Google Search (#7042). + - **Action Required:** Update all references of `groundingAttributions`, `webSearchQueries`, + `retrievalQueries` in `GroundingMetadata` to be non-optional. +- [changed] require at least one argument for `generateContent()`, `generateContentStream()` and + `countTokens()`. +- [feature] Added new overloads for `generateContent()`, `generateContentStream()` and + `countTokens()` that take a `List` parameter. +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. + +# 16.2.0 + +- [changed] Deprecate the `totalBillableCharacters` field (only usable with pre-2.0 models). (#7042) +- [feature] Added support for extra schema properties like `title`, `minItems`, `maxItems`, + `minimum` and `maximum`. As well as support for the `anyOf` schema. (#7013) + +# 16.1.0 + +- [fixed] Fixed `FirebaseAI.getInstance` StackOverflowException (#6971) +- [fixed] Fixed an issue that was causing the SDK to send empty `FunctionDeclaration` descriptions + to the API. +- [changed] Introduced the `Voice` class, which accepts a voice name, and deprecated the `Voices` + class. +- [changed] **Breaking Change**: Updated `SpeechConfig` to take in `Voice` class instead of `Voices` + class. + - **Action Required:** Update all references of `SpeechConfig` initialization to use `Voice` + class. +- [fixed] Fix incorrect model name in count token requests to the developer API backend + # 16.0.0 -* [feature] Initial release of the Firebase AI SDK (`firebase-ai`). This SDK *replaces* the previous - Vertex AI in Firebase SDK (`firebase-vertexai`) to accommodate the evolving set of supported - features and services. - * The new Firebase AI SDK provides **Preview** support for the Gemini Developer API, including its - free tier offering. - * Using the Firebase AI SDK with the Vertex AI Gemini API is still generally available (GA). - - If you're using the old `firebase-vertexai`, we recommend - [migrating to `firebase-ai`](/docs/ai-logic/migrate-to-latest-sdk) - because all new development and features will be in this new SDK. -* [feature] **Preview:** Added support for specifying response modalities in `GenerationConfig` - (only available in the new `firebase-ai` package). This includes support for image generation using - [specific Gemini models](/docs/vertex-ai/models). - - Note: This feature is in Public Preview, which means that it is not subject to any SLA or - deprecation policy and could change in backwards-incompatible ways. +- [feature] Initial release of the Firebase AI SDK (`firebase-ai`). This SDK _replaces_ the previous + Vertex AI in Firebase SDK (`firebase-vertexai`) to accommodate the evolving set of supported + features and services. + - The new Firebase AI SDK provides **Preview** support for the Gemini Developer API, including its + free tier offering. + - Using the Firebase AI SDK with the Vertex AI Gemini API is still generally available (GA). + +If you're using the old `firebase-vertexai`, we recommend +[migrating to `firebase-ai`](/docs/ai-logic/migrate-to-latest-sdk) because all new development and +features will be in this new SDK. + +- [feature] **Preview:** Added support for specifying response modalities in `GenerationConfig` + (only available in the new `firebase-ai` package). This includes support for image generation + using [specific Gemini models](/docs/vertex-ai/models). + +Note: This feature is in Public Preview, which means that it is not subject to any SLA or +deprecation policy and could change in backwards-incompatible ways. diff --git a/firebase-ai/README.md b/firebase-ai/README.md index e09f65c6092..2572fd8da18 100644 --- a/firebase-ai/README.md +++ b/firebase-ai/README.md @@ -1,7 +1,7 @@ # Firebase AI SDK -For developer documentation, please visit https://firebase.google.com/docs/vertex-ai. -This README is for contributors building and running tests for the SDK. +For developer documentation, please visit https://firebase.google.com/docs/vertex-ai. This README is +for contributors building and running tests for the SDK. ## Building @@ -11,9 +11,8 @@ All Gradle commands should be run from the root of this repository. ## Running Tests -> [!IMPORTANT] -> These unit tests require mock response files, which can be downloaded by running -`./firebase-ai/update_responses.sh` from the root of this repository. +> [!IMPORTANT] These unit tests require mock response files, which can be downloaded by running +> `./firebase-ai/update_responses.sh` from the root of this repository. Unit tests: @@ -25,8 +24,8 @@ Integration tests, requiring a running and connected device (emulator or real): ## Code Formatting -Format Kotlin code in this SDK in Android Studio using -the [spotless plugin]([https://plugins.jetbrains.com/plugin/14912-ktfmt](https://github.com/diffplug/spotless) -by running: +Format Kotlin code in this SDK in Android Studio using the [spotless +plugin]([https://plugins.jetbrains.com/plugin/14912-ktfmt](https://github.com/diffplug/spotless) by +running: `./gradlew firebase-ai:spotlessApply` diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index 5645b466110..f73c51d7112 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -23,12 +23,14 @@ package com.google.firebase.ai { method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); method public static com.google.firebase.ai.FirebaseAI getInstance(); method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend, boolean useLimitedUseAppCheckTokens); method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); - method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName); - method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null); - method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null); - method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend, boolean useLimitedUseAppCheckTokens); + method public com.google.firebase.ai.ImagenModel imagenModel(String modelName); + method public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null); + method public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null); + method public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null); @@ -41,31 +43,40 @@ package com.google.firebase.ai { public static final class FirebaseAI.Companion { method public com.google.firebase.ai.FirebaseAI getInstance(); method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend, boolean useLimitedUseAppCheckTokens); method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend, boolean useLimitedUseAppCheckTokens); property public final com.google.firebase.ai.FirebaseAI instance; } public final class FirebaseAIKt { method public static com.google.firebase.ai.FirebaseAI ai(com.google.firebase.Firebase, com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend = GenerativeBackend.googleAI()); + method public static com.google.firebase.ai.FirebaseAI ai(com.google.firebase.Firebase, com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend = GenerativeBackend.googleAI(), boolean useLimitedUseAppCheckTokens); method public static com.google.firebase.ai.FirebaseAI getAi(com.google.firebase.Firebase); } public final class GenerativeModel { method public suspend Object? countTokens(android.graphics.Bitmap prompt, kotlin.coroutines.Continuation); - method public suspend Object? countTokens(com.google.firebase.ai.type.Content[] prompt, kotlin.coroutines.Continuation); + method public suspend Object? countTokens(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content[] prompts, kotlin.coroutines.Continuation); method public suspend Object? countTokens(String prompt, kotlin.coroutines.Continuation); + method public suspend Object? countTokens(java.util.List prompt, kotlin.coroutines.Continuation); method public suspend Object? generateContent(android.graphics.Bitmap prompt, kotlin.coroutines.Continuation); - method public suspend Object? generateContent(com.google.firebase.ai.type.Content[] prompt, kotlin.coroutines.Continuation); + method public suspend Object? generateContent(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content[] prompts, kotlin.coroutines.Continuation); method public suspend Object? generateContent(String prompt, kotlin.coroutines.Continuation); + method public suspend Object? generateContent(java.util.List prompt, kotlin.coroutines.Continuation); method public kotlinx.coroutines.flow.Flow generateContentStream(android.graphics.Bitmap prompt); - method public kotlinx.coroutines.flow.Flow generateContentStream(com.google.firebase.ai.type.Content... prompt); + method public kotlinx.coroutines.flow.Flow generateContentStream(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content... prompts); method public kotlinx.coroutines.flow.Flow generateContentStream(String prompt); + method public kotlinx.coroutines.flow.Flow generateContentStream(java.util.List prompt); method public com.google.firebase.ai.Chat startChat(java.util.List history = emptyList()); } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenModel { + public final class ImagenModel { + method @com.google.firebase.ai.type.PublicPreviewAPI public suspend Object? editImage(java.util.List referenceImages, String prompt, com.google.firebase.ai.type.ImagenEditingConfig? config = null, kotlin.coroutines.Continuation>); method public suspend Object? generateImages(String prompt, kotlin.coroutines.Continuation>); + method @com.google.firebase.ai.type.PublicPreviewAPI public suspend Object? inpaintImage(com.google.firebase.ai.type.ImagenInlineImage image, String prompt, com.google.firebase.ai.type.ImagenMaskReference mask, com.google.firebase.ai.type.ImagenEditingConfig config, kotlin.coroutines.Continuation>); + method @com.google.firebase.ai.type.PublicPreviewAPI public suspend Object? outpaintImage(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, String prompt = "", com.google.firebase.ai.type.ImagenEditingConfig? config = null, kotlin.coroutines.Continuation>); } @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveGenerativeModel { @@ -89,10 +100,10 @@ package com.google.firebase.ai.java { } public abstract class GenerativeModelFutures { - method public abstract com.google.common.util.concurrent.ListenableFuture countTokens(com.google.firebase.ai.type.Content... prompt); + method public abstract com.google.common.util.concurrent.ListenableFuture countTokens(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content... prompts); method public static final com.google.firebase.ai.java.GenerativeModelFutures from(com.google.firebase.ai.GenerativeModel model); - method public abstract com.google.common.util.concurrent.ListenableFuture generateContent(com.google.firebase.ai.type.Content... prompt); - method public abstract org.reactivestreams.Publisher generateContentStream(com.google.firebase.ai.type.Content... prompt); + method public abstract com.google.common.util.concurrent.ListenableFuture generateContent(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content... prompts); + method public abstract org.reactivestreams.Publisher generateContentStream(com.google.firebase.ai.type.Content prompt, com.google.firebase.ai.type.Content... prompts); method public abstract com.google.firebase.ai.GenerativeModel getGenerativeModel(); method public abstract com.google.firebase.ai.java.ChatFutures startChat(); method public abstract com.google.firebase.ai.java.ChatFutures startChat(java.util.List history); @@ -104,9 +115,13 @@ package com.google.firebase.ai.java { } @com.google.firebase.ai.type.PublicPreviewAPI public abstract class ImagenModelFutures { + method public abstract com.google.common.util.concurrent.ListenableFuture> editImage(java.util.List referenceImages, String prompt); + method public abstract com.google.common.util.concurrent.ListenableFuture> editImage(java.util.List referenceImages, String prompt, com.google.firebase.ai.type.ImagenEditingConfig? config = null); method public static final com.google.firebase.ai.java.ImagenModelFutures from(com.google.firebase.ai.ImagenModel model); method public abstract com.google.common.util.concurrent.ListenableFuture> generateImages(String prompt); method public abstract com.google.firebase.ai.ImagenModel getImageModel(); + method public abstract com.google.common.util.concurrent.ListenableFuture> inpaintImage(com.google.firebase.ai.type.ImagenInlineImage image, String prompt, com.google.firebase.ai.type.ImagenMaskReference mask, com.google.firebase.ai.type.ImagenEditingConfig config); + method public abstract com.google.common.util.concurrent.ListenableFuture> outpaintImage(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, String prompt = "", com.google.firebase.ai.type.ImagenEditingConfig? config = null); field public static final com.google.firebase.ai.java.ImagenModelFutures.Companion Companion; } @@ -130,10 +145,18 @@ package com.google.firebase.ai.java { method public abstract org.reactivestreams.Publisher receive(); method public abstract com.google.common.util.concurrent.ListenableFuture send(com.google.firebase.ai.type.Content content); method public abstract com.google.common.util.concurrent.ListenableFuture send(String text); + method public abstract com.google.common.util.concurrent.ListenableFuture sendAudioRealtime(com.google.firebase.ai.type.InlineData audio); method public abstract com.google.common.util.concurrent.ListenableFuture sendFunctionResponse(java.util.List functionList); - method public abstract com.google.common.util.concurrent.ListenableFuture sendMediaStream(java.util.List mediaChunks); + method @Deprecated public abstract com.google.common.util.concurrent.ListenableFuture sendMediaStream(java.util.List mediaChunks); + method public abstract com.google.common.util.concurrent.ListenableFuture sendTextRealtime(String text); + method public abstract com.google.common.util.concurrent.ListenableFuture sendVideoRealtime(com.google.firebase.ai.type.InlineData video); method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(); - method public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(boolean enableInterruptions); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler, boolean enableInterruptions); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler, kotlin.jvm.functions.Function2? transcriptHandler, boolean enableInterruptions); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function2? transcriptHandler); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture startAudioConversation(kotlin.jvm.functions.Function2? transcriptHandler, boolean enableInterruptions); method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public abstract com.google.common.util.concurrent.ListenableFuture stopAudioConversation(); method public abstract void stopReceiving(); field public static final com.google.firebase.ai.java.LiveSessionFutures.Companion Companion; @@ -147,10 +170,17 @@ package com.google.firebase.ai.java { package com.google.firebase.ai.type { + public final class APINotConfiguredException extends com.google.firebase.ai.type.FirebaseAIException { + } + public final class AudioRecordInitializationFailedException extends com.google.firebase.ai.type.FirebaseAIException { ctor public AudioRecordInitializationFailedException(String message); } + public final class AudioTranscriptionConfig { + ctor public AudioTranscriptionConfig(); + } + public final class BlockReason { method public String getName(); method public int getOrdinal(); @@ -171,11 +201,15 @@ package com.google.firebase.ai.type { method public com.google.firebase.ai.type.CitationMetadata? getCitationMetadata(); method public com.google.firebase.ai.type.Content getContent(); method public com.google.firebase.ai.type.FinishReason? getFinishReason(); + method public com.google.firebase.ai.type.GroundingMetadata? getGroundingMetadata(); method public java.util.List getSafetyRatings(); + method public com.google.firebase.ai.type.UrlContextMetadata? getUrlContextMetadata(); property public final com.google.firebase.ai.type.CitationMetadata? citationMetadata; property public final com.google.firebase.ai.type.Content content; property public final com.google.firebase.ai.type.FinishReason? finishReason; + property public final com.google.firebase.ai.type.GroundingMetadata? groundingMetadata; property public final java.util.List safetyRatings; + property public final com.google.firebase.ai.type.UrlContextMetadata? urlContextMetadata; } public final class Citation { @@ -198,6 +232,17 @@ package com.google.firebase.ai.type { property public final java.util.List citations; } + public final class CodeExecutionResultPart implements com.google.firebase.ai.type.Part { + ctor @Deprecated public CodeExecutionResultPart(String outcome, String output); + method public boolean executionSucceeded(); + method public String getOutcome(); + method public String getOutput(); + method public boolean isThought(); + property public boolean isThought; + property public final String outcome; + property public final String output; + } + public final class Content { ctor public Content(String? role = "user", java.util.List parts); ctor public Content(java.util.List parts); @@ -245,22 +290,42 @@ package com.google.firebase.ai.type { } public final class CountTokensResponse { - ctor public CountTokensResponse(int totalTokens, Integer? totalBillableCharacters = null, java.util.List promptTokensDetails = emptyList()); + ctor public CountTokensResponse(int totalTokens, @Deprecated Integer? totalBillableCharacters = null, java.util.List promptTokensDetails = emptyList()); method public operator int component1(); method public operator Integer? component2(); method public operator java.util.List? component3(); method public java.util.List getPromptTokensDetails(); - method public Integer? getTotalBillableCharacters(); + method @Deprecated public Integer? getTotalBillableCharacters(); method public int getTotalTokens(); property public final java.util.List promptTokensDetails; - property public final Integer? totalBillableCharacters; + property @Deprecated public final Integer? totalBillableCharacters; property public final int totalTokens; } + public final class Dimensions { + ctor public Dimensions(int width, int height); + method public int getHeight(); + method public int getWidth(); + property public final int height; + property public final int width; + } + + public final class ExecutableCodePart implements com.google.firebase.ai.type.Part { + ctor @Deprecated public ExecutableCodePart(String language, String code); + method public String getCode(); + method public String getLanguage(); + method public boolean isThought(); + property public final String code; + property public boolean isThought; + property public final String language; + } + public final class FileDataPart implements com.google.firebase.ai.type.Part { ctor public FileDataPart(String uri, String mimeType); method public String getMimeType(); method public String getUri(); + method public boolean isThought(); + property public boolean isThought; property public final String mimeType; property public final String uri; } @@ -295,8 +360,10 @@ package com.google.firebase.ai.type { method public java.util.Map getArgs(); method public String? getId(); method public String getName(); + method public boolean isThought(); property public final java.util.Map args; property public final String? id; + property public boolean isThought; property public final String name; } @@ -325,7 +392,9 @@ package com.google.firebase.ai.type { method public String? getId(); method public String getName(); method public kotlinx.serialization.json.JsonObject getResponse(); + method public boolean isThought(); property public final String? id; + property public boolean isThought; property public final String name; property public final kotlinx.serialization.json.JsonObject response; } @@ -337,12 +406,14 @@ package com.google.firebase.ai.type { method public java.util.List getInlineDataParts(); method public com.google.firebase.ai.type.PromptFeedback? getPromptFeedback(); method public String? getText(); + method public String? getThoughtSummary(); method public com.google.firebase.ai.type.UsageMetadata? getUsageMetadata(); property public final java.util.List candidates; property public final java.util.List functionCalls; property public final java.util.List inlineDataParts; property public final com.google.firebase.ai.type.PromptFeedback? promptFeedback; property public final String? text; + property public final String? thoughtSummary; property public final com.google.firebase.ai.type.UsageMetadata? usageMetadata; } @@ -362,6 +433,7 @@ package com.google.firebase.ai.type { method public com.google.firebase.ai.type.GenerationConfig.Builder setResponseSchema(com.google.firebase.ai.type.Schema? responseSchema); method public com.google.firebase.ai.type.GenerationConfig.Builder setStopSequences(java.util.List? stopSequences); method public com.google.firebase.ai.type.GenerationConfig.Builder setTemperature(Float? temperature); + method public com.google.firebase.ai.type.GenerationConfig.Builder setThinkingConfig(com.google.firebase.ai.type.ThinkingConfig? thinkingConfig); method public com.google.firebase.ai.type.GenerationConfig.Builder setTopK(Integer? topK); method public com.google.firebase.ai.type.GenerationConfig.Builder setTopP(Float? topP); field public Integer? candidateCount; @@ -373,6 +445,7 @@ package com.google.firebase.ai.type { field public com.google.firebase.ai.type.Schema? responseSchema; field public java.util.List? stopSequences; field public Float? temperature; + field public com.google.firebase.ai.type.ThinkingConfig? thinkingConfig; field public Integer? topK; field public Float? topP; } @@ -398,6 +471,48 @@ package com.google.firebase.ai.type { method public com.google.firebase.ai.type.GenerativeBackend vertexAI(String location = "us-central1"); } + public final class GoogleSearch { + ctor public GoogleSearch(); + } + + @Deprecated public final class GroundingAttribution { + ctor @Deprecated public GroundingAttribution(com.google.firebase.ai.type.Segment segment, Float? confidenceScore); + method @Deprecated public Float? getConfidenceScore(); + method @Deprecated public com.google.firebase.ai.type.Segment getSegment(); + property @Deprecated public final Float? confidenceScore; + property @Deprecated public final com.google.firebase.ai.type.Segment segment; + } + + public final class GroundingChunk { + ctor public GroundingChunk(com.google.firebase.ai.type.WebGroundingChunk? web); + method public com.google.firebase.ai.type.WebGroundingChunk? getWeb(); + property public final com.google.firebase.ai.type.WebGroundingChunk? web; + } + + public final class GroundingMetadata { + ctor public GroundingMetadata(java.util.List webSearchQueries, com.google.firebase.ai.type.SearchEntryPoint? searchEntryPoint, java.util.List retrievalQueries, @Deprecated java.util.List groundingAttribution, java.util.List groundingChunks, java.util.List groundingSupports); + method @Deprecated public java.util.List getGroundingAttribution(); + method public java.util.List getGroundingChunks(); + method public java.util.List getGroundingSupports(); + method public java.util.List getRetrievalQueries(); + method public com.google.firebase.ai.type.SearchEntryPoint? getSearchEntryPoint(); + method public java.util.List getWebSearchQueries(); + property @Deprecated public final java.util.List groundingAttribution; + property public final java.util.List groundingChunks; + property public final java.util.List groundingSupports; + property public final java.util.List retrievalQueries; + property public final com.google.firebase.ai.type.SearchEntryPoint? searchEntryPoint; + property public final java.util.List webSearchQueries; + } + + public final class GroundingSupport { + ctor public GroundingSupport(com.google.firebase.ai.type.Segment segment, java.util.List groundingChunkIndices); + method public java.util.List getGroundingChunkIndices(); + method public com.google.firebase.ai.type.Segment getSegment(); + property public final java.util.List groundingChunkIndices; + property public final com.google.firebase.ai.type.Segment segment; + } + public final class HarmBlockMethod { method public int getOrdinal(); property public final int ordinal; @@ -431,6 +546,10 @@ package com.google.firebase.ai.type { field public static final com.google.firebase.ai.type.HarmCategory DANGEROUS_CONTENT; field public static final com.google.firebase.ai.type.HarmCategory HARASSMENT; field public static final com.google.firebase.ai.type.HarmCategory HATE_SPEECH; + field public static final com.google.firebase.ai.type.HarmCategory IMAGE_DANGEROUS_CONTENT; + field public static final com.google.firebase.ai.type.HarmCategory IMAGE_HARASSMENT; + field public static final com.google.firebase.ai.type.HarmCategory IMAGE_HATE; + field public static final com.google.firebase.ai.type.HarmCategory IMAGE_SEXUALLY_EXPLICIT; field public static final com.google.firebase.ai.type.HarmCategory SEXUALLY_EXPLICIT; field public static final com.google.firebase.ai.type.HarmCategory UNKNOWN; } @@ -469,10 +588,12 @@ package com.google.firebase.ai.type { public final class ImagePart implements com.google.firebase.ai.type.Part { ctor public ImagePart(android.graphics.Bitmap image); method public android.graphics.Bitmap getImage(); + method public boolean isThought(); property public final android.graphics.Bitmap image; + property public boolean isThought; } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenAspectRatio { + public final class ImagenAspectRatio { field public static final com.google.firebase.ai.type.ImagenAspectRatio.Companion Companion; field public static final com.google.firebase.ai.type.ImagenAspectRatio LANDSCAPE_16x9; field public static final com.google.firebase.ai.type.ImagenAspectRatio LANDSCAPE_4x3; @@ -484,7 +605,44 @@ package com.google.firebase.ai.type { public static final class ImagenAspectRatio.Companion { } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenGenerationConfig { + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenBackgroundMask extends com.google.firebase.ai.type.ImagenMaskReference { + ctor public ImagenBackgroundMask(Double? dilation = null); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenControlReference extends com.google.firebase.ai.type.ImagenReferenceImage { + ctor public ImagenControlReference(com.google.firebase.ai.type.ImagenControlType type, com.google.firebase.ai.type.ImagenInlineImage? image = null, Integer? referenceId = null, Boolean? enableComputation = null, Integer? superpixelRegionSize = null, Integer? superpixelRuler = null); + } + + public final class ImagenControlType { + field public static final com.google.firebase.ai.type.ImagenControlType CANNY; + field public static final com.google.firebase.ai.type.ImagenControlType COLOR_SUPERPIXEL; + field public static final com.google.firebase.ai.type.ImagenControlType.Companion Companion; + field public static final com.google.firebase.ai.type.ImagenControlType FACE_MESH; + field public static final com.google.firebase.ai.type.ImagenControlType SCRIBBLE; + } + + public static final class ImagenControlType.Companion { + } + + public final class ImagenEditMode { + field public static final com.google.firebase.ai.type.ImagenEditMode.Companion Companion; + field public static final com.google.firebase.ai.type.ImagenEditMode INPAINT_INSERTION; + field public static final com.google.firebase.ai.type.ImagenEditMode INPAINT_REMOVAL; + field public static final com.google.firebase.ai.type.ImagenEditMode OUTPAINT; + } + + public static final class ImagenEditMode.Companion { + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenEditingConfig { + ctor public ImagenEditingConfig(com.google.firebase.ai.type.ImagenEditMode? editMode = null, Integer? editSteps = null); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenForegroundMask extends com.google.firebase.ai.type.ImagenMaskReference { + ctor public ImagenForegroundMask(Double? dilation = null); + } + + public final class ImagenGenerationConfig { ctor public ImagenGenerationConfig(String? negativePrompt = null, Integer? numberOfImages = 1, com.google.firebase.ai.type.ImagenAspectRatio? aspectRatio = null, com.google.firebase.ai.type.ImagenImageFormat? imageFormat = null, Boolean? addWatermark = null); method public Boolean? getAddWatermark(); method public com.google.firebase.ai.type.ImagenAspectRatio? getAspectRatio(); @@ -519,17 +677,17 @@ package com.google.firebase.ai.type { } public final class ImagenGenerationConfigKt { - method @com.google.firebase.ai.type.PublicPreviewAPI public static com.google.firebase.ai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 init); + method public static com.google.firebase.ai.type.ImagenGenerationConfig imagenGenerationConfig(kotlin.jvm.functions.Function1 init); } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenGenerationResponse { + public final class ImagenGenerationResponse { method public String? getFilteredReason(); method public java.util.List getImages(); property public final String? filteredReason; property public final java.util.List images; } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenImageFormat { + public final class ImagenImageFormat { method public Integer? getCompressionQuality(); method public String getMimeType(); method public static com.google.firebase.ai.type.ImagenImageFormat jpeg(Integer? compressionQuality = null); @@ -544,7 +702,29 @@ package com.google.firebase.ai.type { method public com.google.firebase.ai.type.ImagenImageFormat png(); } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenInlineImage { + public final class ImagenImagePlacement { + method public static com.google.firebase.ai.type.ImagenImagePlacement fromCoordinate(int x, int y); + method public Integer? getX(); + method public Integer? getY(); + property public final Integer? x; + property public final Integer? y; + field public static final com.google.firebase.ai.type.ImagenImagePlacement BOTTOM_CENTER; + field public static final com.google.firebase.ai.type.ImagenImagePlacement BOTTOM_LEFT; + field public static final com.google.firebase.ai.type.ImagenImagePlacement BOTTOM_RIGHT; + field public static final com.google.firebase.ai.type.ImagenImagePlacement CENTER; + field public static final com.google.firebase.ai.type.ImagenImagePlacement.Companion Companion; + field public static final com.google.firebase.ai.type.ImagenImagePlacement LEFT_CENTER; + field public static final com.google.firebase.ai.type.ImagenImagePlacement RIGHT_CENTER; + field public static final com.google.firebase.ai.type.ImagenImagePlacement TOP_CENTER; + field public static final com.google.firebase.ai.type.ImagenImagePlacement TOP_LEFT; + field public static final com.google.firebase.ai.type.ImagenImagePlacement TOP_RIGHT; + } + + public static final class ImagenImagePlacement.Companion { + method public com.google.firebase.ai.type.ImagenImagePlacement fromCoordinate(int x, int y); + } + + public final class ImagenInlineImage { method public android.graphics.Bitmap asBitmap(); method public byte[] getData(); method public String getMimeType(); @@ -552,7 +732,24 @@ package com.google.firebase.ai.type { property public final String mimeType; } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenPersonFilterLevel { + public final class ImagenInlineImageKt { + method @com.google.firebase.ai.type.PublicPreviewAPI public static com.google.firebase.ai.type.ImagenInlineImage toImagenInlineImage(android.graphics.Bitmap); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public abstract class ImagenMaskReference extends com.google.firebase.ai.type.ImagenReferenceImage { + method public static final java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions); + method public static final java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER); + method public static final java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, double dilation = 0.01); + field public static final com.google.firebase.ai.type.ImagenMaskReference.Companion Companion; + } + + public static final class ImagenMaskReference.Companion { + method public java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions); + method public java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER); + method public java.util.List generateMaskAndPadForOutpainting(com.google.firebase.ai.type.ImagenInlineImage image, com.google.firebase.ai.type.Dimensions newDimensions, com.google.firebase.ai.type.ImagenImagePlacement newPosition = com.google.firebase.ai.type.ImagenImagePlacement.CENTER, double dilation = 0.01); + } + + public final class ImagenPersonFilterLevel { field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel ALLOW_ADULT; field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel ALLOW_ALL; field public static final com.google.firebase.ai.type.ImagenPersonFilterLevel BLOCK_ALL; @@ -562,7 +759,22 @@ package com.google.firebase.ai.type { public static final class ImagenPersonFilterLevel.Companion { } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSafetyFilterLevel { + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenRawImage extends com.google.firebase.ai.type.ImagenReferenceImage { + ctor public ImagenRawImage(com.google.firebase.ai.type.ImagenInlineImage image); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenRawMask extends com.google.firebase.ai.type.ImagenMaskReference { + ctor public ImagenRawMask(com.google.firebase.ai.type.ImagenInlineImage mask, Double? dilation = null); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public abstract class ImagenReferenceImage { + method public final com.google.firebase.ai.type.ImagenInlineImage? getImage(); + method public final Integer? getReferenceId(); + property public final com.google.firebase.ai.type.ImagenInlineImage? image; + property public final Integer? referenceId; + } + + public final class ImagenSafetyFilterLevel { field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_LOW_AND_ABOVE; field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_MEDIUM_AND_ABOVE; field public static final com.google.firebase.ai.type.ImagenSafetyFilterLevel BLOCK_NONE; @@ -573,15 +785,47 @@ package com.google.firebase.ai.type { public static final class ImagenSafetyFilterLevel.Companion { } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSafetySettings { + public final class ImagenSafetySettings { ctor public ImagenSafetySettings(com.google.firebase.ai.type.ImagenSafetyFilterLevel safetyFilterLevel, com.google.firebase.ai.type.ImagenPersonFilterLevel personFilterLevel); } + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSemanticMask extends com.google.firebase.ai.type.ImagenMaskReference { + ctor public ImagenSemanticMask(java.util.List classes, Double? dilation = null); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenStyleReference extends com.google.firebase.ai.type.ImagenReferenceImage { + ctor public ImagenStyleReference(com.google.firebase.ai.type.ImagenInlineImage image, Integer? referenceId = null, String? description = null); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class ImagenSubjectReference extends com.google.firebase.ai.type.ImagenReferenceImage { + ctor public ImagenSubjectReference(com.google.firebase.ai.type.ImagenInlineImage image, Integer? referenceId = null, String? description = null, com.google.firebase.ai.type.ImagenSubjectReferenceType? subjectType = null); + } + + public final class ImagenSubjectReferenceType { + field public static final com.google.firebase.ai.type.ImagenSubjectReferenceType ANIMAL; + field public static final com.google.firebase.ai.type.ImagenSubjectReferenceType.Companion Companion; + field public static final com.google.firebase.ai.type.ImagenSubjectReferenceType PERSON; + field public static final com.google.firebase.ai.type.ImagenSubjectReferenceType PRODUCT; + } + + public static final class ImagenSubjectReferenceType.Companion { + } + + public final class InlineData { + ctor public InlineData(byte[] data, String mimeType); + method public byte[] getData(); + method public String getMimeType(); + property public final byte[] data; + property public final String mimeType; + } + public final class InlineDataPart implements com.google.firebase.ai.type.Part { ctor public InlineDataPart(byte[] inlineData, String mimeType); method public byte[] getInlineData(); method public String getMimeType(); + method public boolean isThought(); property public final byte[] inlineData; + property public boolean isThought; property public final String mimeType; } @@ -601,18 +845,20 @@ package com.google.firebase.ai.type { public static final class LiveGenerationConfig.Builder { ctor public LiveGenerationConfig.Builder(); method public com.google.firebase.ai.type.LiveGenerationConfig build(); - method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setCandidateCount(Integer? candidateCount); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setFrequencyPenalty(Float? frequencyPenalty); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setInputAudioTranscription(com.google.firebase.ai.type.AudioTranscriptionConfig? config); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setMaxOutputTokens(Integer? maxOutputTokens); + method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setOutputAudioTranscription(com.google.firebase.ai.type.AudioTranscriptionConfig? config); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setPresencePenalty(Float? presencePenalty); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setResponseModality(com.google.firebase.ai.type.ResponseModality? responseModality); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setSpeechConfig(com.google.firebase.ai.type.SpeechConfig? speechConfig); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTemperature(Float? temperature); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTopK(Integer? topK); method public com.google.firebase.ai.type.LiveGenerationConfig.Builder setTopP(Float? topP); - field public Integer? candidateCount; field public Float? frequencyPenalty; + field public com.google.firebase.ai.type.AudioTranscriptionConfig? inputAudioTranscription; field public Integer? maxOutputTokens; + field public com.google.firebase.ai.type.AudioTranscriptionConfig? outputAudioTranscription; field public Float? presencePenalty; field public com.google.firebase.ai.type.ResponseModality? responseModality; field public com.google.firebase.ai.type.SpeechConfig? speechConfig; @@ -630,14 +876,18 @@ package com.google.firebase.ai.type { } @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveServerContent implements com.google.firebase.ai.type.LiveServerMessage { - ctor public LiveServerContent(com.google.firebase.ai.type.Content? content, boolean interrupted, boolean turnComplete, boolean generationComplete); + ctor @Deprecated public LiveServerContent(com.google.firebase.ai.type.Content? content, boolean interrupted, boolean turnComplete, boolean generationComplete, com.google.firebase.ai.type.Transcription? inputTranscription, com.google.firebase.ai.type.Transcription? outputTranscription); method public com.google.firebase.ai.type.Content? getContent(); method public boolean getGenerationComplete(); + method public com.google.firebase.ai.type.Transcription? getInputTranscription(); method public boolean getInterrupted(); + method public com.google.firebase.ai.type.Transcription? getOutputTranscription(); method public boolean getTurnComplete(); property public final com.google.firebase.ai.type.Content? content; property public final boolean generationComplete; + property public final com.google.firebase.ai.type.Transcription? inputTranscription; property public final boolean interrupted; + property public final com.google.firebase.ai.type.Transcription? outputTranscription; property public final boolean turnComplete; } @@ -662,22 +912,29 @@ package com.google.firebase.ai.type { @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveSession { method public suspend Object? close(kotlin.coroutines.Continuation); + method public boolean isAudioConversationActive(); + method public boolean isClosed(); method public kotlinx.coroutines.flow.Flow receive(); method public suspend Object? send(com.google.firebase.ai.type.Content content, kotlin.coroutines.Continuation); method public suspend Object? send(String text, kotlin.coroutines.Continuation); + method public suspend Object? sendAudioRealtime(com.google.firebase.ai.type.InlineData audio, kotlin.coroutines.Continuation); method public suspend Object? sendFunctionResponse(java.util.List functionList, kotlin.coroutines.Continuation); - method public suspend Object? sendMediaStream(java.util.List mediaChunks, kotlin.coroutines.Continuation); + method @Deprecated public suspend Object? sendMediaStream(java.util.List mediaChunks, kotlin.coroutines.Continuation); + method public suspend Object? sendTextRealtime(String text, kotlin.coroutines.Continuation); + method public suspend Object? sendVideoRealtime(com.google.firebase.ai.type.InlineData video, kotlin.coroutines.Continuation); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public suspend Object? startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler = null, boolean enableInterruptions = false, kotlin.coroutines.Continuation); method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public suspend Object? startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler = null, kotlin.coroutines.Continuation); + method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public suspend Object? startAudioConversation(kotlin.jvm.functions.Function1? functionCallHandler = null, kotlin.jvm.functions.Function2? transcriptHandler = null, boolean enableInterruptions = false, kotlin.coroutines.Continuation); method public void stopAudioConversation(); method public void stopReceiving(); } - @com.google.firebase.ai.type.PublicPreviewAPI public final class MediaData { - ctor public MediaData(byte[] data, String mimeType); - method public byte[] getData(); - method public String getMimeType(); - property public final byte[] data; - property public final String mimeType; + @Deprecated @com.google.firebase.ai.type.PublicPreviewAPI public final class MediaData { + ctor @Deprecated public MediaData(byte[] data, String mimeType); + method @Deprecated public byte[] getData(); + method @Deprecated public String getMimeType(); + property @Deprecated public final byte[] data; + property @Deprecated public final String mimeType; } public final class ModalityTokenCount { @@ -690,6 +947,8 @@ package com.google.firebase.ai.type { } public interface Part { + method public boolean isThought(); + property public abstract boolean isThought; } public final class PartKt { @@ -699,6 +958,10 @@ package com.google.firebase.ai.type { method public static String? asTextOrNull(com.google.firebase.ai.type.Part); } + public final class PermissionMissingException extends com.google.firebase.ai.type.FirebaseAIException { + ctor public PermissionMissingException(String message, Throwable? cause = null); + } + public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseAIException { method public com.google.firebase.ai.type.GenerateContentResponse? getResponse(); property public final com.google.firebase.ai.type.GenerateContentResponse? response; @@ -714,7 +977,7 @@ package com.google.firebase.ai.type { property public final java.util.List safetyRatings; } - @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface PublicPreviewAPI { + @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface PublicPreviewAPI { } public final class QuotaExceededException extends com.google.firebase.ai.type.FirebaseAIException { @@ -765,84 +1028,156 @@ package com.google.firebase.ai.type { } public final class Schema { + method public static com.google.firebase.ai.type.Schema anyOf(java.util.List schemas); method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items); method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null); method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null); + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null, Integer? minItems = null); + method public static com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null, Integer? minItems = null, Integer? maxItems = null); method public static com.google.firebase.ai.type.Schema boolean(); method public static com.google.firebase.ai.type.Schema boolean(String? description = null); method public static com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false, String? title = null); method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values); method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null); method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false, String? title = null); + method public java.util.List? getAnyOf(); method public String? getDescription(); method public java.util.List? getEnum(); method public String? getFormat(); method public com.google.firebase.ai.type.Schema? getItems(); + method public Integer? getMaxItems(); + method public Double? getMaximum(); + method public Integer? getMinItems(); + method public Double? getMinimum(); method public Boolean? getNullable(); method public java.util.Map? getProperties(); method public java.util.List? getRequired(); + method public String? getTitle(); method public String getType(); method public static com.google.firebase.ai.type.Schema numDouble(); method public static com.google.firebase.ai.type.Schema numDouble(String? description = null); method public static com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null); + method public static com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public static com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public static com.google.firebase.ai.type.Schema numFloat(); method public static com.google.firebase.ai.type.Schema numFloat(String? description = null); method public static com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null); + method public static com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public static com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public static com.google.firebase.ai.type.Schema numInt(); method public static com.google.firebase.ai.type.Schema numInt(String? description = null); method public static com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null); + method public static com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public static com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public static com.google.firebase.ai.type.Schema numLong(); method public static com.google.firebase.ai.type.Schema numLong(String? description = null); method public static com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null); + method public static com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public static com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties); method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList()); method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null); method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false); + method public static com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false, String? title = null); method public static com.google.firebase.ai.type.Schema str(); method public static com.google.firebase.ai.type.Schema str(String? description = null); method public static com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false); method public static com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null); + method public static com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null, String? title = null); + property public final java.util.List? anyOf; property public final String? description; property public final java.util.List? enum; property public final String? format; property public final com.google.firebase.ai.type.Schema? items; + property public final Integer? maxItems; + property public final Double? maximum; + property public final Integer? minItems; + property public final Double? minimum; property public final Boolean? nullable; property public final java.util.Map? properties; property public final java.util.List? required; + property public final String? title; property public final String type; field public static final com.google.firebase.ai.type.Schema.Companion Companion; } public static final class Schema.Companion { + method public com.google.firebase.ai.type.Schema anyOf(java.util.List schemas); method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items); method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null); method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null); + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null, Integer? minItems = null); + method public com.google.firebase.ai.type.Schema array(com.google.firebase.ai.type.Schema items, String? description = null, boolean nullable = false, String? title = null, Integer? minItems = null, Integer? maxItems = null); method public com.google.firebase.ai.type.Schema boolean(); method public com.google.firebase.ai.type.Schema boolean(String? description = null); method public com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema boolean(String? description = null, boolean nullable = false, String? title = null); method public com.google.firebase.ai.type.Schema enumeration(java.util.List values); method public com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null); method public com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema enumeration(java.util.List values, String? description = null, boolean nullable = false, String? title = null); method public com.google.firebase.ai.type.Schema numDouble(); method public com.google.firebase.ai.type.Schema numDouble(String? description = null); method public com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null); + method public com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public com.google.firebase.ai.type.Schema numDouble(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public com.google.firebase.ai.type.Schema numFloat(); method public com.google.firebase.ai.type.Schema numFloat(String? description = null); method public com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null); + method public com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public com.google.firebase.ai.type.Schema numFloat(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public com.google.firebase.ai.type.Schema numInt(); method public com.google.firebase.ai.type.Schema numInt(String? description = null); method public com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null); + method public com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public com.google.firebase.ai.type.Schema numInt(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public com.google.firebase.ai.type.Schema numLong(); method public com.google.firebase.ai.type.Schema numLong(String? description = null); method public com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null); + method public com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null); + method public com.google.firebase.ai.type.Schema numLong(String? description = null, boolean nullable = false, String? title = null, Double? minimum = null, Double? maximum = null); method public com.google.firebase.ai.type.Schema obj(java.util.Map properties); method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList()); method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null); method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false); + method public com.google.firebase.ai.type.Schema obj(java.util.Map properties, java.util.List optionalProperties = emptyList(), String? description = null, boolean nullable = false, String? title = null); method public com.google.firebase.ai.type.Schema str(); method public com.google.firebase.ai.type.Schema str(String? description = null); method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false); method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null); + method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null, String? title = null); + } + + public final class SearchEntryPoint { + ctor public SearchEntryPoint(String renderedContent, String? sdkBlob); + method public String getRenderedContent(); + method public String? getSdkBlob(); + property public final String renderedContent; + property public final String? sdkBlob; + } + + public final class Segment { + ctor public Segment(int startIndex, int endIndex, int partIndex, String text); + method public int getEndIndex(); + method public int getPartIndex(); + method public int getStartIndex(); + method public String getText(); + property public final int endIndex; + property public final int partIndex; + property public final int startIndex; + property public final String text; } public final class SerializationException extends com.google.firebase.ai.type.FirebaseAIException { @@ -878,39 +1213,104 @@ package com.google.firebase.ai.type { public final class TextPart implements com.google.firebase.ai.type.Part { ctor public TextPart(String text); method public String getText(); + method public boolean isThought(); + property public boolean isThought; property public final String text; } + public final class ThinkingConfig { + } + + public static final class ThinkingConfig.Builder { + ctor public ThinkingConfig.Builder(); + method public com.google.firebase.ai.type.ThinkingConfig build(); + method public com.google.firebase.ai.type.ThinkingConfig.Builder setIncludeThoughts(boolean includeThoughts); + method public com.google.firebase.ai.type.ThinkingConfig.Builder setThinkingBudget(int thinkingBudget); + } + + public final class ThinkingConfigKt { + method public static com.google.firebase.ai.type.ThinkingConfig thinkingConfig(kotlin.jvm.functions.Function1 init); + } + public final class Tool { + method public static com.google.firebase.ai.type.Tool codeExecution(); method public static com.google.firebase.ai.type.Tool functionDeclarations(java.util.List functionDeclarations); + method public static com.google.firebase.ai.type.Tool googleSearch(com.google.firebase.ai.type.GoogleSearch googleSearch = com.google.firebase.ai.type.GoogleSearch()); + method @com.google.firebase.ai.type.PublicPreviewAPI public static com.google.firebase.ai.type.Tool urlContext(com.google.firebase.ai.type.UrlContext urlContext = com.google.firebase.ai.type.UrlContext()); field public static final com.google.firebase.ai.type.Tool.Companion Companion; } public static final class Tool.Companion { + method public com.google.firebase.ai.type.Tool codeExecution(); method public com.google.firebase.ai.type.Tool functionDeclarations(java.util.List functionDeclarations); + method public com.google.firebase.ai.type.Tool googleSearch(com.google.firebase.ai.type.GoogleSearch googleSearch = com.google.firebase.ai.type.GoogleSearch()); + method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.type.Tool urlContext(com.google.firebase.ai.type.UrlContext urlContext = com.google.firebase.ai.type.UrlContext()); } public final class ToolConfig { ctor public ToolConfig(com.google.firebase.ai.type.FunctionCallingConfig? functionCallingConfig); } + public final class Transcription { + method public String? getText(); + property public final String? text; + } + public final class UnknownException extends com.google.firebase.ai.type.FirebaseAIException { } public final class UnsupportedUserLocationException extends com.google.firebase.ai.type.FirebaseAIException { } + @com.google.firebase.ai.type.PublicPreviewAPI public final class UrlContext { + ctor public UrlContext(); + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class UrlContextMetadata { + method public java.util.List getUrlMetadata(); + property public final java.util.List urlMetadata; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class UrlMetadata { + method public String? getRetrievedUrl(); + method public com.google.firebase.ai.type.UrlRetrievalStatus getUrlRetrievalStatus(); + property public final String? retrievedUrl; + property public final com.google.firebase.ai.type.UrlRetrievalStatus urlRetrievalStatus; + } + + @com.google.firebase.ai.type.PublicPreviewAPI public final class UrlRetrievalStatus { + method public String getName(); + method public int getOrdinal(); + property public final String name; + property public final int ordinal; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus.Companion Companion; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus ERROR; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus PAYWALL; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus SUCCESS; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus UNSAFE; + field public static final com.google.firebase.ai.type.UrlRetrievalStatus UNSPECIFIED; + } + + public static final class UrlRetrievalStatus.Companion { + } + public final class UsageMetadata { - ctor public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List promptTokensDetails, java.util.List candidatesTokensDetails); + ctor @Deprecated public UsageMetadata(int promptTokenCount, Integer? candidatesTokenCount, int totalTokenCount, java.util.List promptTokensDetails, java.util.List candidatesTokensDetails, int thoughtsTokenCount); method public Integer? getCandidatesTokenCount(); method public java.util.List getCandidatesTokensDetails(); method public int getPromptTokenCount(); method public java.util.List getPromptTokensDetails(); + method public int getThoughtsTokenCount(); + method public int getToolUsePromptTokenCount(); + method public java.util.List getToolUsePromptTokensDetails(); method public int getTotalTokenCount(); property public final Integer? candidatesTokenCount; property public final java.util.List candidatesTokensDetails; property public final int promptTokenCount; property public final java.util.List promptTokensDetails; + property public final int thoughtsTokenCount; + property public final int toolUsePromptTokenCount; + property public final java.util.List toolUsePromptTokensDetails; property public final int totalTokenCount; } @@ -935,5 +1335,15 @@ package com.google.firebase.ai.type { @Deprecated public static final class Voices.Companion { } + public final class WebGroundingChunk { + ctor public WebGroundingChunk(String? uri, String? title, String? domain); + method public String? getDomain(); + method public String? getTitle(); + method public String? getUri(); + property public final String? domain; + property public final String? title; + property public final String? uri; + } + } diff --git a/firebase-ai/firebase-ai.gradle.kts b/firebase-ai/firebase-ai.gradle.kts index de29f44b0a1..2aa3a16b6fb 100644 --- a/firebase-ai/firebase-ai.gradle.kts +++ b/firebase-ai/firebase-ai.gradle.kts @@ -16,11 +16,12 @@ @file:Suppress("UnstableApiUsage") -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("firebase-library") id("kotlin-android") + id("copy-google-services") alias(libs.plugins.kotlinx.serialization) } @@ -28,9 +29,8 @@ firebaseLibrary { testLab.enabled = false publishJavadoc = true releaseNotes { - name.set("{{firebase_ai}}") + name.set("{{firebase_ai_logic}}") versionName.set("ai") - hasKTX.set(false) } } @@ -40,7 +40,7 @@ android { namespace = "com.google.firebase.ai" compileSdk = 34 defaultConfig { - minSdk = 21 + minSdk = rootProject.extra["minSdkVersion"] as Int consumerProguardFiles("consumer-rules.pro") multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -55,7 +55,6 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { jvmTarget = "1.8" } testOptions { targetSdk = targetSdkVersion unitTests { @@ -70,18 +69,9 @@ android { sourceSets { getByName("test").java.srcDirs("src/testUtil") } } -// Enable Kotlin "Explicit API Mode". This causes the Kotlin compiler to fail if any -// classes, methods, or properties have implicit `public` visibility. This check helps -// avoid accidentally leaking elements into the public API, requiring that any public -// element be explicitly declared as `public`. -// https://github.com/Kotlin/KEEP/blob/master/proposals/explicit-api-mode.md -// https://chao2zhang.medium.com/explicit-api-mode-for-kotlin-on-android-b8264fdd76d1 -tasks.withType().all { - if (!name.contains("test", ignoreCase = true)) { - if (!kotlinOptions.freeCompilerArgs.contains("-Xexplicit-api=strict")) { - kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict" - } - } +kotlin { + compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } + explicitApi() } dependencies { @@ -92,9 +82,9 @@ dependencies { implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.logging) - api("com.google.firebase:firebase-common:21.0.0") - implementation("com.google.firebase:firebase-components:18.0.0") - implementation("com.google.firebase:firebase-annotations:16.2.0") + api(libs.firebase.common) + implementation(libs.firebase.components) + implementation(libs.firebase.annotations) implementation("com.google.firebase:firebase-appcheck-interop:17.1.0") implementation(libs.androidx.annotation) implementation(libs.kotlinx.serialization.json) diff --git a/firebase-ai/gradle.properties b/firebase-ai/gradle.properties index 1c7c87996dd..c4acd5b3aae 100644 --- a/firebase-ai/gradle.properties +++ b/firebase-ai/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.1.0 -latestReleasedVersion=16.0.0 +version=17.5.0 +latestReleasedVersion=17.4.0 diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/AIModels.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/AIModels.kt new file mode 100644 index 00000000000..5b51b47fc0c --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/AIModels.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2025 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.ai + +import androidx.test.platform.app.InstrumentationRegistry +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.type.GenerativeBackend + +class AIModels { + + companion object { + private val API_KEY: String = "" + private val APP_ID: String = "" + private val PROJECT_ID: String = "fireescape-integ-tests" + // General purpose models + var app: FirebaseApp? = null + var flash2Model: GenerativeModel? = null + var flash2LiteModel: GenerativeModel? = null + + /** Returns a list of general purpose models to test */ + fun getModels(): List { + if (flash2Model == null) { + setup() + } + return listOf(flash2Model!!, flash2LiteModel!!) + } + + fun app(): FirebaseApp { + if (app == null) { + setup() + } + return app!! + } + + fun setup() { + val context = InstrumentationRegistry.getInstrumentation().context + app = FirebaseApp.initializeApp(context) + flash2Model = + FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI()) + .generativeModel( + modelName = "gemini-2.5-flash", + ) + flash2LiteModel = + FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI()) + .generativeModel( + modelName = "gemini-2.5-flash-lite", + ) + } + } +} diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/CountTokensTests.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/CountTokensTests.kt new file mode 100644 index 00000000000..04ff6262ee8 --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/CountTokensTests.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2025 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.ai + +import android.graphics.Bitmap +import com.google.firebase.ai.AIModels.Companion.getModels +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.ContentModality +import com.google.firebase.ai.type.CountTokensResponse +import java.io.ByteArrayOutputStream +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class CountTokensTests { + + /** Ensures that the token count is expected for simple words. */ + @Test + fun testCountTokensAmount() { + for (model in getModels()) { + runBlocking { + val response = model.countTokens("this is five different words") + assert(response.totalTokens == 5) + assert(response.promptTokensDetails.size == 1) + assert(response.promptTokensDetails[0].modality == ContentModality.TEXT) + assert(response.promptTokensDetails[0].tokenCount == 5) + } + } + } + + /** Ensures that the model returns token counts in the correct modality for text. */ + @Test + fun testCountTokensTextModality() { + for (model in getModels()) { + runBlocking { + val response = model.countTokens("this is a text prompt") + checkTokenCountsMatch(response) + assert(response.promptTokensDetails.size == 1) + assert(containsModality(response, ContentModality.TEXT)) + } + } + } + + /** Ensures that the model returns token counts in the correct modality for bitmap images. */ + @Test + fun testCountTokensImageModality() { + for (model in getModels()) { + runBlocking { + val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val response = model.countTokens(bitmap) + checkTokenCountsMatch(response) + assert(response.promptTokensDetails.size == 1) + assert(containsModality(response, ContentModality.IMAGE)) + } + } + } + + /** + * Ensures the model can count tokens for multiple modalities at once, and return the + * corresponding token modalities correctly. + */ + @Test + fun testCountTokensTextAndImageModality() { + for (model in getModels()) { + runBlocking { + val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val response = + model.countTokens( + Content.Builder().text("this is text").build(), + Content.Builder().image(bitmap).build() + ) + checkTokenCountsMatch(response) + assert(response.promptTokensDetails.size == 2) + assert(containsModality(response, ContentModality.TEXT)) + assert(containsModality(response, ContentModality.IMAGE)) + } + } + } + + /** + * Ensures the model can count the tokens for a sent file. Additionally, ensures that the model + * treats this sent file as the modality of the mime type, in this case, a plaintext file has its + * tokens counted as `ContentModality.TEXT`. + */ + @Test + fun testCountTokensTextFileModality() { + for (model in getModels()) { + runBlocking { + val response = + model.countTokens( + Content.Builder().inlineData("this is text".toByteArray(), "text/plain").build() + ) + checkTokenCountsMatch(response) + assert(response.totalTokens == 3) + assert(response.promptTokensDetails.size == 1) + assert(containsModality(response, ContentModality.TEXT)) + } + } + } + + /** + * Ensures the model can count the tokens for a sent file. Additionally, ensures that the model + * treats this sent file as the modality of the mime type, in this case, a PNG encoded bitmap has + * its tokens counted as `ContentModality.IMAGE`. + */ + @Test + fun testCountTokensImageFileModality() { + for (model in getModels()) { + runBlocking { + val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 1, stream) + val array = stream.toByteArray() + val response = model.countTokens(Content.Builder().inlineData(array, "image/png").build()) + checkTokenCountsMatch(response) + assert(response.promptTokensDetails.size == 1) + assert(containsModality(response, ContentModality.IMAGE)) + } + } + } + + /** + * Ensures that nothing is free, that is, empty content contains no tokens. For some reason, this + * is treated as `ContentModality.TEXT`. + */ + @Test + fun testCountTokensNothingIsFree() { + for (model in getModels()) { + runBlocking { + val response = model.countTokens(Content.Builder().build()) + checkTokenCountsMatch(response) + assert(response.totalTokens == 0) + assert(response.promptTokensDetails.size == 1) + assert(containsModality(response, ContentModality.TEXT)) + } + } + } + + /** + * Checks if the model can count the tokens for a sent file. Additionally, ensures that the model + * treats this sent file as the modality of the mime type, in this case, a JSON file is not + * recognized, and no tokens are counted. This ensures if/when the model can handle JSON, our + * testing makes us aware. + */ + @Test + fun testCountTokensJsonFileModality() { + for (model in getModels()) { + runBlocking { + val json = + """ + { + "foo": "bar", + "baz": 3, + "qux": [ + { + "quux": [ + 1, + 2 + ] + } + ] + } + """ + .trimIndent() + val response = + model.countTokens( + Content.Builder().inlineData(json.toByteArray(), "application/json").build() + ) + checkTokenCountsMatch(response) + assert(response.promptTokensDetails.isEmpty()) + assert(response.totalTokens == 0) + } + } + } + + fun checkTokenCountsMatch(response: CountTokensResponse) { + assert(sumTokenCount(response) == response.totalTokens) + } + + fun sumTokenCount(response: CountTokensResponse): Int { + return response.promptTokensDetails.sumOf { it.tokenCount } + } + + fun containsModality(response: CountTokensResponse, modality: ContentModality): Boolean { + for (token in response.promptTokensDetails) { + if (token.modality == modality) { + return true + } + } + return false + } +} diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/GenerateContentTests.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/GenerateContentTests.kt new file mode 100644 index 00000000000..7de068311ef --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/GenerateContentTests.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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.ai + +import android.graphics.Bitmap +import com.google.firebase.ai.AIModels.Companion.getModels +import com.google.firebase.ai.type.Content +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class GenerateContentTests { + private val validator = TypesValidator() + + /** + * Ensures the model can response to prompts and that the structure of this response is expected. + */ + @Test + fun testGenerateContent_BasicRequest() { + for (model in getModels()) { + runBlocking { + val response = model.generateContent("pick a random color") + validator.validateResponse(response) + } + } + } + + /** + * Ensures that the model can answer very simple questions. Further testing the "logic" of the + * model and the content of the responses is prone to flaking, this test is also prone to that. + * This is probably the furthest we can consistently test for reasonable response structure, past + * sending the request and response back to the model and asking it if it fits our expectations. + */ + @Test + fun testGenerateContent_ColorMixing() { + for (model in getModels()) { + runBlocking { + val response = model.generateContent("what color is created when red and yellow are mixed?") + validator.validateResponse(response) + assert(response.text!!.contains("orange", true)) + } + } + } + + /** + * Ensures that the model can answer very simple questions. Further testing the "logic" of the + * model and the content of the responses is prone to flaking, this test is also prone to that. + * This is probably the furthest we can consistently test for reasonable response structure, past + * sending the request and response back to the model and asking it if it fits our expectations. + */ + @Test + fun testGenerateContent_CanSendImage() { + for (model in getModels()) { + runBlocking { + val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) + val yellow = Integer.parseUnsignedInt("FFFFFF00", 16) + bitmap.setPixel(3, 3, yellow) + bitmap.setPixel(6, 3, yellow) + bitmap.setPixel(3, 6, yellow) + bitmap.setPixel(4, 7, yellow) + bitmap.setPixel(5, 7, yellow) + bitmap.setPixel(6, 6, yellow) + val response = + model.generateContent( + Content.Builder().text("here is a tiny smile").image(bitmap).build() + ) + validator.validateResponse(response) + } + } + } +} diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ImagenTests.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ImagenTests.kt new file mode 100644 index 00000000000..218637d4b32 --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ImagenTests.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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.ai + +import com.google.firebase.ai.AIModels.Companion.app +import com.google.firebase.ai.type.ImagenBackgroundMask +import com.google.firebase.ai.type.ImagenEditMode +import com.google.firebase.ai.type.ImagenEditingConfig +import com.google.firebase.ai.type.ImagenRawImage +import com.google.firebase.ai.type.PublicPreviewAPI +import kotlinx.coroutines.runBlocking +import org.junit.Ignore +import org.junit.Test + +@OptIn(PublicPreviewAPI::class) +class ImagenTests { + + @Ignore("Currently not supported by backend model") + @Test + fun testGenerateAndEditImage() { + val imageGenerationModel = FirebaseAI.getInstance(app()).imagenModel("imagen-3.0-generate-002") + val imageEditingModel = FirebaseAI.getInstance(app()).imagenModel("imagen-3.0-capability-001") + + runBlocking { + val catImage = imageGenerationModel.generateImages("A cat").images.first() + val editedCatImage = + imageEditingModel.editImage( + listOf(ImagenRawImage(catImage), ImagenBackgroundMask()), + "A cat flying through space", + ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION) + ) + assert(editedCatImage.images.size == 1) + } + } +} diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ToolTests.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ToolTests.kt new file mode 100644 index 00000000000..856154c161f --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/ToolTests.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2025 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.ai + +import com.google.firebase.ai.AIModels.Companion.app +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.FunctionCallingConfig +import com.google.firebase.ai.type.FunctionDeclaration +import com.google.firebase.ai.type.FunctionResponsePart +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.Schema +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.ai.type.content +import io.ktor.util.toLowerCasePreservingASCIIRules +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.float +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Test + +class ToolTests { + val validator = TypesValidator() + + @Test + fun testTools_functionCallStructuring() { + val schema = + mapOf( + Pair( + "character", + Schema.obj( + mapOf( + Pair("name", Schema.string("the character's full name")), + Pair("gender", Schema.string("the character's gender")), + Pair("weight", Schema.float("the character's weight, in kilograms")), + Pair("height", Schema.float("the character's height, in centimeters")), + Pair( + "favorite_foods", + Schema.array( + Schema.string("the name of a food"), + "a short list of the character's favorite foods" + ) + ), + Pair( + "mother", + Schema.obj( + mapOf(Pair("name", Schema.string("the character's mother's name"))), + description = "information about the character's mother" + ) + ), + ) + ) + ), + ) + val model = + setupModel( + FunctionDeclaration( + name = "getFavoriteColor", + description = + "determines a video game character's favorite color based on their features", + parameters = schema + ), + ) + runBlocking { + val response = + model.generateContent( + "I'm imagining a video game character whose name is sam, but I can't think of the rest of their traits, could you make them up for me and figure out the character's favorite color?" + ) + validator.validateResponse((response)) + assert(response.functionCalls.size == 1) + val call = response.functionCalls[0] + assert(call.name == "getFavoriteColor") + validateSchema(schema, call.args) + } + } + + @Test + fun testTools_basicDecisionMaking() { + val schema = + mapOf( + Pair("character", Schema.string("the character whose favorite color should be obtained")) + ) + val model = + setupModel( + FunctionDeclaration( + name = "getFavoriteColor", + description = "returns the favorite color from a provided character's name", + parameters = schema + ), + FunctionDeclaration( + name = "eatAllSnacks", + description = + "orders a robot to find the kitchen of the provided character by their name, then eat all of their snacks so they get really sad. returns how many snacks were eaten", + parameters = schema + ) + ) + runBlocking { + val response = model.generateContent("what is amy's favorite color?") + validator.validateResponse((response)) + assert(response.functionCalls.size == 1) + val call = response.functionCalls[0] + assert(call.name == "getFavoriteColor") + validateSchema(schema, call.args) + } + } + + /** Ensures the model is capable of a simple question, tool call, response workflow. */ + @Test + fun testTools_BasicToolCall() { + val schema = + mapOf( + Pair("character", Schema.string("the character whose favorite color should be obtained")) + ) + val model = + setupModel( + FunctionDeclaration( + name = "getFavoriteColor", + description = "returns the favorite color from a provided character's name", + parameters = schema + ) + ) + runBlocking { + val question = content { text("what's bob's favorite color?") } + val response = model.generateContent(question) + validator.validateResponse((response)) + assert(response.functionCalls.size == 1) + for (call in response.functionCalls) { + assert(call.name == "getFavoriteColor") + validateSchema(schema, call.args) + assert( + call.args["character"]!!.jsonPrimitive.content.toLowerCasePreservingASCIIRules() == "bob" + ) + model.generateContent( + question, + Content( + role = "model", + parts = + listOf( + call, + ) + ), + Content( + parts = + listOf( + FunctionResponsePart( + id = call.id, + name = call.name, + response = JsonObject(mapOf(Pair("result", JsonPrimitive("green")))) + ), + ) + ) + ) + } + } + } + + /** + * Ensures the model can chain function calls together to reach trivial conclusions. In this case, + * the model needs to use the output of one function call as the input to another. + */ + @Test + fun testTools_sequencingFunctionCalls() { + val nameSchema = + mapOf( + Pair("name", Schema.string("the name of the person whose birth month should be obtained")) + ) + val monthSchema = + mapOf(Pair("month", Schema.string("the month whose color should be obtained"))) + val model = + setupModel( + FunctionDeclaration( + name = "getBirthMonth", + description = "returns a person's birth month based on their name", + parameters = nameSchema + ), + FunctionDeclaration( + name = "getMonthColor", + description = "returns the color for a certain month", + parameters = monthSchema + ) + ) + runBlocking { + val question = content { text("what color is john's birth month") } + val response = model.generateContent(question) + assert(response.functionCalls.size == 1) + val call = response.functionCalls[0] + assert(call.name == "getBirthMonth") + assert(call.args["name"]!!.jsonPrimitive.content.toLowerCasePreservingASCIIRules() == "john") + validateSchema(nameSchema, call.args) + val response2 = + model.generateContent( + question, + Content( + role = "model", + parts = + listOf( + call, + ) + ), + Content( + parts = + listOf( + FunctionResponsePart( + id = call.id, + name = call.name, + response = JsonObject(mapOf(Pair("result", JsonPrimitive("june")))) + ), + ) + ) + ) + validator.validateResponse((response)) + assert(response2.functionCalls.size == 1) + val call2 = response2.functionCalls[0] + assert(call2.name == "getMonthColor") + assert( + call2.args["month"]!!.jsonPrimitive.content.toLowerCasePreservingASCIIRules() == "june" + ) + validateSchema(monthSchema, call2.args) + } + } + + fun validateSchema(schema: Map, args: Map) { + // Model should not call the function with unspecified arguments + assert(schema.keys.containsAll(args.keys)) + for (entry in schema) { + validateSchema(entry.value, args.get(entry.key)) + } + } + + /** Simple schema validation. Not comprehensive, but should detect notable inaccuracy. */ + fun validateSchema(schema: Schema, json: JsonElement?) { + if (json == null) { + assert(schema.nullable == true) + return + } + when (json) { + is JsonNull -> { + assert(schema.nullable == true) + } + is JsonPrimitive -> { + if (schema.type == "INTEGER") { + assert(json.intOrNull != null) + } else if (schema.type == "NUMBER") { + assert(json.doubleOrNull != null) + } else if (schema.type == "BOOLEAN") { + assert(json.booleanOrNull != null) + } else if (schema.type == "STRING") { + assert(json.isString) + } else { + assert(false) + } + } + is JsonObject -> { + assert(schema.type == "OBJECT") + val required = schema.required ?: listOf() + val obj = json.jsonObject + for (entry in schema.properties!!) { + if (obj.containsKey(entry.key)) { + validateSchema(entry.value, obj.get(entry.key)) + } else { + assert(!required.contains(entry.key)) + } + } + } + is JsonArray -> { + assert(schema.type == "ARRAY") + for (e in json.jsonArray) { + validateSchema(schema.items!!, e) + } + } + } + } + + companion object { + @JvmStatic + fun setupModel(vararg functions: FunctionDeclaration): GenerativeModel { + val model = + FirebaseAI.getInstance(app(), GenerativeBackend.vertexAI()) + .generativeModel( + modelName = "gemini-2.5-flash", + toolConfig = + ToolConfig( + functionCallingConfig = FunctionCallingConfig(FunctionCallingConfig.Mode.ANY) + ), + tools = listOf(Tool.functionDeclarations(functions.toList())), + ) + return model + } + } +} diff --git a/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/TypesValidator.kt b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/TypesValidator.kt new file mode 100644 index 00000000000..768b9cf4eca --- /dev/null +++ b/firebase-ai/src/androidTest/kotlin/com/google/firebase/ai/TypesValidator.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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.ai + +import com.google.firebase.ai.type.Candidate +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.TextPart + +/** Performs structural validation of various API types */ +class TypesValidator { + + fun validateResponse(response: GenerateContentResponse) { + if (response.candidates.isNotEmpty() && hasText(response.candidates[0].content)) { + assert(response.text!!.isNotEmpty()) + } else if (response.candidates.isNotEmpty()) { + assert(!hasText(response.candidates[0].content)) + } + response.candidates.forEach { validateCandidate(it) } + } + + fun validateCandidate(candidate: Candidate) { + validateContent(candidate.content) + } + + fun validateContent(content: Content) { + assert(content.role != "user") + } + + fun hasText(content: Content): Boolean { + return content.parts.filterIsInstance().isNotEmpty() + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt index 13599fb1c9a..73d304d3885 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/Chat.kt @@ -66,7 +66,8 @@ public class Chat( prompt.assertComesFromUser() attemptLock() try { - val response = model.generateContent(*history.toTypedArray(), prompt) + val fullPrompt = history + prompt + val response = model.generateContent(fullPrompt.first(), *fullPrompt.drop(1).toTypedArray()) history.add(prompt) history.add(response.candidates.first().content) return response @@ -127,7 +128,8 @@ public class Chat( prompt.assertComesFromUser() attemptLock() - val flow = model.generateContentStream(*history.toTypedArray(), prompt) + val fullPrompt = history + prompt + val flow = model.generateContentStream(fullPrompt.first(), *fullPrompt.drop(1).toTypedArray()) val bitmaps = LinkedList() val inlineDataParts = LinkedList() val text = StringBuilder() diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt index 86eb8057b1d..dd2309c984a 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt @@ -25,7 +25,6 @@ import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.GenerativeBackendEnum import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenSafetySettings -import com.google.firebase.ai.type.InvalidStateException import com.google.firebase.ai.type.LiveGenerationConfig import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions @@ -47,12 +46,14 @@ internal constructor( @Blocking private val blockingDispatcher: CoroutineContext, private val appCheckProvider: Provider, private val internalAuthProvider: Provider, + private val useLimitedUseAppCheckTokens: Boolean ) { /** * Instantiates a new [GenerativeModel] given the provided parameters. * - * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. + * @param modelName The name of the model to use. See the documentation for a list of + * [supported models](https://firebase.google.com/docs/ai-logic/models). * @param generationConfig The configuration parameters to use for content generation. * @param safetySettings The safety bounds the model will abide to during content generation. * @param tools A list of [Tool]s the model may use to generate content. @@ -92,6 +93,7 @@ internal constructor( modelUri, firebaseApp.options.apiKey, firebaseApp, + useLimitedUseAppCheckTokens, generationConfig, safetySettings, tools, @@ -107,7 +109,8 @@ internal constructor( /** * Instantiates a new [LiveGenerationConfig] given the provided parameters. * - * @param modelName The name of the model to use, for example `"gemini-2.0-flash-exp"`. + * @param modelName The name of the model to use. See the documentation for a list of + * [supported models](https://firebase.google.com/docs/ai-logic/models). * @param generationConfig The configuration parameters to use for content generation. * @param tools A list of [Tool]s the model may use to generate content. * @param systemInstruction [Content] instructions that direct the model to behave a certain way. @@ -124,6 +127,7 @@ internal constructor( systemInstruction: Content? = null, requestOptions: RequestOptions = RequestOptions(), ): LiveGenerativeModel { + if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { Log.w( TAG, @@ -138,7 +142,7 @@ internal constructor( GenerativeBackendEnum.VERTEX_AI -> "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" GenerativeBackendEnum.GOOGLE_AI -> - throw InvalidStateException("Live Model is not yet available on the Google AI backend") + "projects/${firebaseApp.options.projectId}/models/${modelName}" }, firebaseApp.options.apiKey, firebaseApp, @@ -150,20 +154,22 @@ internal constructor( requestOptions, appCheckProvider.get(), internalAuthProvider.get(), + backend, + useLimitedUseAppCheckTokens, ) } /** * Instantiates a new [ImagenModel] given the provided parameters. * - * @param modelName The name of the model to use, for example `"imagen-3.0-generate-001"`. + * @param modelName The name of the model to use. See the documentation for a list of + * [supported models](https://firebase.google.com/docs/ai-logic/models). * @param generationConfig The configuration parameters to use for image generation. * @param safetySettings The safety bounds the model will abide by during image generation. * @param requestOptions Configuration options for sending requests to the backend. * @return The initialized [ImagenModel] instance. */ @JvmOverloads - @PublicPreviewAPI public fun imagenModel( modelName: String, generationConfig: ImagenGenerationConfig? = null, @@ -190,6 +196,7 @@ internal constructor( modelUri, firebaseApp.options.apiKey, firebaseApp, + useLimitedUseAppCheckTokens, generationConfig, safetySettings, requestOptions, @@ -214,9 +221,30 @@ internal constructor( public fun getInstance( app: FirebaseApp = Firebase.app, backend: GenerativeBackend + ): FirebaseAI { + return getInstance(app, backend, false) + } + + /** + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. + * + * @param backend the backend reference to make generative AI requests to. + * @param useLimitedUseAppCheckTokens when sending tokens to the backend, this option enables + * the usage of App Check's limited-use tokens instead of the standard cached tokens. Learn more + * about [limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check), including + * their nuances, when to use them, and best practices for integrating them into your app. + * + * _This flag is set to `false` by default._ + */ + @JvmStatic + @JvmOverloads + public fun getInstance( + app: FirebaseApp = Firebase.app, + backend: GenerativeBackend, + useLimitedUseAppCheckTokens: Boolean, ): FirebaseAI { val multiResourceComponent = app[FirebaseAIMultiResourceComponent::class.java] - return multiResourceComponent.get(backend) + return multiResourceComponent.get(InstanceKey(backend, useLimitedUseAppCheckTokens)) } /** The [FirebaseAI] instance for the provided [FirebaseApp] using the Google AI Backend. */ @@ -245,3 +273,19 @@ public fun Firebase.ai( app: FirebaseApp = Firebase.app, backend: GenerativeBackend = GenerativeBackend.googleAI() ): FirebaseAI = FirebaseAI.getInstance(app, backend) + +/** + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. + * + * @param backend the backend reference to make generative AI requests to. + * @param useLimitedUseAppCheckTokens use App Check's limited-use tokens when sending requests to + * the backend. Learn more about + * [limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check), including their + * nuances, when to use them, and best practices for integrating them into your app. + */ +// TODO(b/440356335): Update docs above when web page goes live in M170 +public fun Firebase.ai( + app: FirebaseApp = Firebase.app, + backend: GenerativeBackend = GenerativeBackend.googleAI(), + useLimitedUseAppCheckTokens: Boolean +): FirebaseAI = FirebaseAI.getInstance(app, backend, useLimitedUseAppCheckTokens) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt index c0667b1685e..d39a93fa598 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt @@ -37,18 +37,24 @@ internal class FirebaseAIMultiResourceComponent( private val internalAuthProvider: Provider, ) { - @GuardedBy("this") private val instances: MutableMap = mutableMapOf() + @GuardedBy("this") private val instances: MutableMap = mutableMapOf() - fun get(backend: GenerativeBackend): FirebaseAI = + fun get(key: InstanceKey): FirebaseAI = synchronized(this) { - instances[backend.location] - ?: FirebaseAI( - app, - backend, - blockingDispatcher, - appCheckProvider, - internalAuthProvider, - ) - .also { instances[backend.location] = it } + instances.getOrPut(key) { + FirebaseAI( + app, + key.backend, + blockingDispatcher, + appCheckProvider, + internalAuthProvider, + key.useLimitedUseAppCheckTokens + ) + } } } + +internal data class InstanceKey( + val backend: GenerativeBackend, + val useLimitedUseAppCheckTokens: Boolean +) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt index 1b36998f970..45aa1e567e3 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt @@ -65,6 +65,7 @@ internal constructor( modelName: String, apiKey: String, firebaseApp: FirebaseApp, + useLimitedUseAppCheckTokens: Boolean, generationConfig: GenerationConfig? = null, safetySettings: List? = null, tools: List? = null, @@ -73,7 +74,7 @@ internal constructor( requestOptions: RequestOptions = RequestOptions(), generativeBackend: GenerativeBackend, appCheckTokenProvider: InteropAppCheckTokenProvider? = null, - internalAuthProvider: InternalAuthProvider? = null, + internalAuthProvider: InternalAuthProvider? = null ) : this( modelName, generationConfig, @@ -88,7 +89,12 @@ internal constructor( requestOptions, "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", firebaseApp, - AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + AppCheckHeaderProvider( + TAG, + useLimitedUseAppCheckTokens, + appCheckTokenProvider, + internalAuthProvider + ), ), ) @@ -100,13 +106,48 @@ internal constructor( * @throws [FirebaseAIException] if the request failed. * @see [FirebaseAIException] for types of errors. */ - public suspend fun generateContent(vararg prompt: Content): GenerateContentResponse = + public suspend fun generateContent( + prompt: Content, + vararg prompts: Content + ): GenerateContentResponse = try { - controller.generateContent(constructRequest(*prompt)).toPublic().validate() + controller.generateContent(constructRequest(prompt, *prompts)).toPublic().validate() } catch (e: Throwable) { throw FirebaseAIException.from(e) } + /** + * Generates new content from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The content generated by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun generateContent(prompt: List): GenerateContentResponse = + try { + controller.generateContent(constructRequest(prompt)).toPublic().validate() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + + /** + * Generates new content as a stream from the input [Content] given to the model as a prompt. + * + * @param prompt The input(s) given to the model as a prompt. + * @return A [Flow] which will emit responses as they are returned by the model. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public fun generateContentStream( + prompt: Content, + vararg prompts: Content + ): Flow = + controller + .generateContentStream(constructRequest(prompt, *prompts)) + .catch { throw FirebaseAIException.from(it) } + .map { it.toPublic().validate() } + /** * Generates new content as a stream from the input [Content] given to the model as a prompt. * @@ -115,9 +156,9 @@ internal constructor( * @throws [FirebaseAIException] if the request failed. * @see [FirebaseAIException] for types of errors. */ - public fun generateContentStream(vararg prompt: Content): Flow = + public fun generateContentStream(prompt: List): Flow = controller - .generateContentStream(constructRequest(*prompt)) + .generateContentStream(constructRequest(prompt)) .catch { throw FirebaseAIException.from(it) } .map { it.toPublic().validate() } @@ -177,9 +218,25 @@ internal constructor( * @throws [FirebaseAIException] if the request failed. * @see [FirebaseAIException] for types of errors. */ - public suspend fun countTokens(vararg prompt: Content): CountTokensResponse { + public suspend fun countTokens(prompt: Content, vararg prompts: Content): CountTokensResponse { + try { + return controller.countTokens(constructCountTokensRequest(prompt, *prompts)).toPublic() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + } + + /** + * Counts the number of tokens in a prompt using the model's tokenizer. + * + * @param prompt The input(s) given to the model as a prompt. + * @return The [CountTokensResponse] of running the model's tokenizer on the input. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. + */ + public suspend fun countTokens(prompt: List): CountTokensResponse { try { - return controller.countTokens(constructCountTokensRequest(*prompt)).toPublic() + return controller.countTokens(constructCountTokensRequest(*prompt.toTypedArray())).toPublic() } catch (e: Throwable) { throw FirebaseAIException.from(e) } @@ -232,6 +289,8 @@ internal constructor( systemInstruction?.copy(role = "system")?.toInternal(), ) + private fun constructRequest(prompt: List) = constructRequest(*prompt.toTypedArray()) + private fun constructCountTokensRequest(vararg prompt: Content) = when (generativeBackend.backend) { GenerativeBackendEnum.GOOGLE_AI -> CountTokensRequest.forGoogleAI(constructRequest(*prompt)) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt index 4d88d09b1e1..62f11319f68 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt @@ -19,12 +19,19 @@ package com.google.firebase.ai import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider -import com.google.firebase.ai.common.ContentBlockedException import com.google.firebase.ai.common.GenerateImageRequest +import com.google.firebase.ai.type.ContentBlockedException +import com.google.firebase.ai.type.Dimensions import com.google.firebase.ai.type.FirebaseAIException +import com.google.firebase.ai.type.ImagenEditMode +import com.google.firebase.ai.type.ImagenEditingConfig import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.ImagenImagePlacement import com.google.firebase.ai.type.ImagenInlineImage +import com.google.firebase.ai.type.ImagenMaskReference +import com.google.firebase.ai.type.ImagenRawImage +import com.google.firebase.ai.type.ImagenReferenceImage import com.google.firebase.ai.type.ImagenSafetySettings import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions @@ -34,8 +41,10 @@ import com.google.firebase.auth.internal.InternalAuthProvider /** * Represents a generative model (like Imagen), capable of generating images based on various input * types. + * + * See the documentation for a list of + * [supported models](https://firebase.google.com/docs/ai-logic/models). */ -@PublicPreviewAPI public class ImagenModel internal constructor( private val modelName: String, @@ -48,6 +57,7 @@ internal constructor( modelName: String, apiKey: String, firebaseApp: FirebaseApp, + useLimitedUseAppCheckTokens: Boolean, generationConfig: ImagenGenerationConfig? = null, safetySettings: ImagenSafetySettings? = null, requestOptions: RequestOptions = RequestOptions(), @@ -63,7 +73,12 @@ internal constructor( requestOptions, "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", firebaseApp, - AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + AppCheckHeaderProvider( + TAG, + useLimitedUseAppCheckTokens, + appCheckTokenProvider, + internalAuthProvider + ), ), ) @@ -75,30 +90,137 @@ internal constructor( public suspend fun generateImages(prompt: String): ImagenGenerationResponse = try { controller - .generateImage(constructRequest(prompt, null, generationConfig)) + .generateImage(constructGenerateImageRequest(prompt, generationConfig)) .validate() .toPublicInline() } catch (e: Throwable) { throw FirebaseAIException.from(e) } - private fun constructRequest( + /** + * Generates an image from a single or set of base images, returning the result directly to the + * caller. + * + * @param referenceImages the image inputs given to the model as a prompt + * @param prompt the text input given to the model as a prompt + * @param config the editing configuration settings + */ + @PublicPreviewAPI + public suspend fun editImage( + referenceImages: List, prompt: String, - gcsUri: String?, - config: ImagenGenerationConfig?, + config: ImagenEditingConfig? = null, + ): ImagenGenerationResponse = + try { + controller + .generateImage(constructEditRequest(referenceImages, prompt, config)) + .validate() + .toPublicInline() + } catch (e: Throwable) { + throw FirebaseAIException.from(e) + } + + /** + * Generates an image by inpainting a masked off part of a base image. Inpainting is the process + * of filling in missing or masked off parts of the image using context from the original image + * and prompt. + * + * @param image the base image + * @param prompt the text input given to the model as a prompt + * @param mask the mask which defines where in the image can be painted by Imagen. + * @param config the editing configuration settings, it should include an [ImagenEditMode] + */ + @PublicPreviewAPI + public suspend fun inpaintImage( + image: ImagenInlineImage, + prompt: String, + mask: ImagenMaskReference, + config: ImagenEditingConfig, + ): ImagenGenerationResponse { + return editImage(listOf(ImagenRawImage(image), mask), prompt, config) + } + + /** + * Generates an image by outpainting the given image, extending its content beyond the original + * borders using context from the original image, and optionally, the prompt. + * + * @param image the base image + * @param newDimensions the new dimensions for the image, *must* be larger than the original + * image. + * @param newPosition the placement of the base image within the new image. This can either be + * coordinates (0,0 is the top left corner) or an alignment (ex: + * [ImagenImagePlacement.BOTTOM_CENTER]) + * @param prompt optional, can be used to specify the background generated if context is + * insufficient + * @param config the editing configuration settings + * @see [ImagenMaskReference.generateMaskAndPadForOutpainting] + */ + @PublicPreviewAPI + public suspend fun outpaintImage( + image: ImagenInlineImage, + newDimensions: Dimensions, + newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER, + prompt: String = "", + config: ImagenEditingConfig? = null, + ): ImagenGenerationResponse { + return editImage( + ImagenMaskReference.generateMaskAndPadForOutpainting(image, newDimensions, newPosition), + prompt, + ImagenEditingConfig(ImagenEditMode.OUTPAINT, config?.editSteps) + ) + } + + private fun constructGenerateImageRequest( + prompt: String, + generationConfig: ImagenGenerationConfig? = null, + ): GenerateImageRequest { + @OptIn(PublicPreviewAPI::class) + return GenerateImageRequest( + listOf(GenerateImageRequest.ImagenPrompt(prompt, null)), + GenerateImageRequest.ImagenParameters( + sampleCount = generationConfig?.numberOfImages ?: 1, + includeRaiReason = true, + includeSafetyAttributes = true, + addWatermark = generationConfig?.addWatermark, + personGeneration = safetySettings?.personFilterLevel?.internalVal, + negativePrompt = generationConfig?.negativePrompt, + safetySetting = safetySettings?.safetyFilterLevel?.internalVal, + storageUri = null, + aspectRatio = generationConfig?.aspectRatio?.internalVal, + imageOutputOptions = generationConfig?.imageFormat?.toInternal(), + editMode = null, + editConfig = null, + ), + ) + } + + @PublicPreviewAPI + private fun constructEditRequest( + referenceImages: List, + prompt: String, + editConfig: ImagenEditingConfig?, ): GenerateImageRequest { + var maxRefId = referenceImages.mapNotNull { it.referenceId }.maxOrNull() ?: 1 return GenerateImageRequest( - listOf(GenerateImageRequest.ImagenPrompt(prompt)), + listOf( + GenerateImageRequest.ImagenPrompt( + prompt = prompt, + referenceImages = referenceImages.map { it.toInternal(++maxRefId) }, + ) + ), GenerateImageRequest.ImagenParameters( - sampleCount = config?.numberOfImages ?: 1, + sampleCount = generationConfig?.numberOfImages ?: 1, includeRaiReason = true, + includeSafetyAttributes = true, addWatermark = generationConfig?.addWatermark, personGeneration = safetySettings?.personFilterLevel?.internalVal, - negativePrompt = config?.negativePrompt, + negativePrompt = generationConfig?.negativePrompt, safetySetting = safetySettings?.safetyFilterLevel?.internalVal, - storageUri = gcsUri, - aspectRatio = config?.aspectRatio?.internalVal, + storageUri = null, + aspectRatio = generationConfig?.aspectRatio?.internalVal, imageOutputOptions = generationConfig?.imageFormat?.toInternal(), + editMode = editConfig?.editMode?.value, + editConfig = editConfig?.toInternal(), ), ) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt index fe4cae0d187..b0a1b541c6b 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt @@ -21,6 +21,7 @@ import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider import com.google.firebase.ai.common.JSON import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.LiveClientSetupMessage import com.google.firebase.ai.type.LiveGenerationConfig import com.google.firebase.ai.type.LiveSession @@ -31,6 +32,7 @@ import com.google.firebase.ai.type.Tool import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.websocket.Frame import io.ktor.websocket.close import io.ktor.websocket.readBytes @@ -54,6 +56,7 @@ internal constructor( private val tools: List? = null, private val systemInstruction: Content? = null, private val location: String, + private val firebaseApp: FirebaseApp, private val controller: APIController ) { internal constructor( @@ -68,6 +71,8 @@ internal constructor( requestOptions: RequestOptions = RequestOptions(), appCheckTokenProvider: InteropAppCheckTokenProvider? = null, internalAuthProvider: InternalAuthProvider? = null, + generativeBackend: GenerativeBackend, + useLimitedUseAppCheckTokens: Boolean, ) : this( modelName, blockingDispatcher, @@ -75,13 +80,20 @@ internal constructor( tools, systemInstruction, location, + firebaseApp, APIController( apiKey, modelName, requestOptions, "gl-kotlin/${KotlinVersion.CURRENT}-ai fire/${BuildConfig.VERSION_NAME}", firebaseApp, - AppCheckHeaderProvider(TAG, appCheckTokenProvider, internalAuthProvider), + AppCheckHeaderProvider( + TAG, + useLimitedUseAppCheckTokens, + appCheckTokenProvider, + internalAuthProvider + ), + generativeBackend ), ) @@ -99,24 +111,34 @@ internal constructor( modelName, config?.toInternal(), tools?.map { it.toInternal() }, - systemInstruction?.toInternal() + systemInstruction?.toInternal(), + config?.inputAudioTranscription?.toInternal(), + config?.outputAudioTranscription?.toInternal() ) .toInternal() val data: String = Json.encodeToString(clientMessage) + var webSession: DefaultClientWebSocketSession? = null try { - val webSession = controller.getWebSocketSession(location) + webSession = controller.getWebSocketSession(location) webSession.send(Frame.Text(data)) val receivedJsonStr = webSession.incoming.receive().readBytes().toString(Charsets.UTF_8) val receivedJson = JSON.parseToJsonElement(receivedJsonStr) return if (receivedJson is JsonObject && "setupComplete" in receivedJson) { - LiveSession(session = webSession, blockingDispatcher = blockingDispatcher) + LiveSession( + session = webSession, + blockingDispatcher = blockingDispatcher, + firebaseApp = firebaseApp + ) } else { webSession.close() throw ServiceConnectionHandshakeFailedException("Unable to connect to the server") } } catch (e: ClosedReceiveChannelException) { - throw ServiceConnectionHandshakeFailedException("Channel was closed by the server", e) + val reason = webSession?.closeReason?.await() + val message = + "Channel was closed by the server.${if (reason != null) " Details: ${reason.message}" else ""}" + throw ServiceConnectionHandshakeFailedException(message, e) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index 34a4b96b7dd..220e5efedac 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -21,14 +21,26 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.util.decodeToFlow import com.google.firebase.ai.common.util.fullModelName +import com.google.firebase.ai.type.APINotConfiguredException import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.FirebaseAIException import com.google.firebase.ai.type.GRpcErrorResponse import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.GenerativeBackendEnum import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.PromptBlockedException import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.QuotaExceededException import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.Response +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.SerializationException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.type.ServiceDisabledException +import com.google.firebase.ai.type.UnsupportedUserLocationException import com.google.firebase.options import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -36,7 +48,7 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.plugins.websocket.webSocketSession import io.ktor.client.request.HttpRequestBuilder @@ -65,6 +77,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.ClassDiscriminatorMode import kotlinx.serialization.json.Json @OptIn(ExperimentalSerializationApi::class) @@ -73,6 +86,7 @@ internal val JSON = Json { prettyPrint = false isLenient = true explicitNulls = false + classDiscriminatorMode = ClassDiscriminatorMode.NONE } /** @@ -99,6 +113,7 @@ internal constructor( private val appVersion: Int = 0, private val googleAppId: String, private val headerProvider: HeaderProvider?, + private val backend: GenerativeBackend? = null ) { constructor( @@ -108,6 +123,7 @@ internal constructor( apiClient: String, firebaseApp: FirebaseApp, headerProvider: HeaderProvider? = null, + backend: GenerativeBackend? = null, ) : this( key, model, @@ -117,7 +133,8 @@ internal constructor( firebaseApp, getVersionNumber(firebaseApp), firebaseApp.options.applicationId, - headerProvider + headerProvider, + backend ) private val model = fullModelName(model) @@ -144,7 +161,7 @@ internal constructor( .body() .validate() } catch (e: Throwable) { - throw FirebaseCommonAIException.from(e) + throw FirebaseAIException.from(e) } suspend fun generateImage(request: GenerateImageRequest): ImagenGenerationResponse.Internal = @@ -157,13 +174,19 @@ internal constructor( .also { validateResponse(it) } .body() } catch (e: Throwable) { - throw FirebaseCommonAIException.from(e) + throw FirebaseAIException.from(e) } private fun getBidiEndpoint(location: String): String = - "wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/$location?key=$key" + when (backend?.backend) { + GenerativeBackendEnum.VERTEX_AI, + null -> + "wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/$location?key=$key" + GenerativeBackendEnum.GOOGLE_AI -> + "wss://firebasevertexai.googleapis.com/ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent?key=$key" + } - suspend fun getWebSocketSession(location: String): ClientWebSocketSession = + suspend fun getWebSocketSession(location: String): DefaultClientWebSocketSession = client.webSocketSession(getBidiEndpoint(location)) { applyCommonHeaders() } fun generateContentStream( @@ -176,7 +199,7 @@ internal constructor( applyCommonConfiguration(request) } .map { it.validate() } - .catch { throw FirebaseCommonAIException.from(it) } + .catch { throw FirebaseAIException.from(it) } suspend fun countTokens(request: CountTokensRequest): CountTokensResponse.Internal = try { @@ -188,7 +211,7 @@ internal constructor( .also { validateResponse(it) } .body() } catch (e: Throwable) { - throw FirebaseCommonAIException.from(e) + throw FirebaseAIException.from(e) } private fun HttpRequestBuilder.applyCommonHeaders() { @@ -323,6 +346,11 @@ private suspend fun validateResponse(response: HttpResponse) { if (message.contains("The prompt could not be submitted")) { throw PromptBlockedException(message) } + if (message.contains("genai config not found")) { + throw APINotConfiguredException( + "The Gemini Developer API is not enabled, to enable and configure, see https://firebase.google.com/docs/ai-logic/faq-and-troubleshooting?api=dev#error-genai-config-not-found" + ) + } getServiceDisabledErrorDetailsOrNull(error)?.let { val errorMessage = if (it.metadata?.get("service") == "firebasevertexai.googleapis.com") { @@ -356,9 +384,9 @@ private fun GenerateContentResponse.Internal.validate() = apply { if ((candidates?.isEmpty() != false) && promptFeedback == null) { throw SerializationException("Error deserializing response, found no valid fields") } - promptFeedback?.blockReason?.let { throw PromptBlockedException(this) } + promptFeedback?.blockReason?.let { throw PromptBlockedException(this.toPublic(), null, null) } candidates ?.mapNotNull { it.finishReason } ?.firstOrNull { it != FinishReason.Internal.STOP } - ?.let { throw ResponseStoppedException(this) } + ?.let { throw ResponseStoppedException(this.toPublic()) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt index d5a5ec32305..96214c98a2d 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/AppCheckHeaderProvider.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.tasks.await internal class AppCheckHeaderProvider( private val logTag: String, + private val useLimitedUseAppCheckTokens: Boolean, private val appCheckTokenProvider: InteropAppCheckTokenProvider? = null, private val internalAuthProvider: InternalAuthProvider? = null, ) : HeaderProvider { @@ -36,7 +37,14 @@ internal class AppCheckHeaderProvider( if (appCheckTokenProvider == null) { Log.w(logTag, "AppCheck not registered, skipping") } else { - val token = appCheckTokenProvider.getToken(false).await() + val result = + if (useLimitedUseAppCheckTokens) { + appCheckTokenProvider.limitedUseToken + } else { + appCheckTokenProvider.getToken(false) + } + + val token = result.await() if (token.error != null) { Log.w(logTag, "Error obtaining AppCheck token", token.error) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt deleted file mode 100644 index 6e2ff67ca4d..00000000000 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Exceptions.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2024 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.ai.common - -import com.google.firebase.ai.type.GenerateContentResponse -import io.ktor.serialization.JsonConvertException -import kotlinx.coroutines.TimeoutCancellationException - -/** Parent class for any errors that occur. */ -internal sealed class FirebaseCommonAIException(message: String, cause: Throwable? = null) : - RuntimeException(message, cause) { - companion object { - - /** - * Converts a [Throwable] to a [FirebaseCommonAIException]. - * - * Will populate default messages as expected, and propagate the provided [cause] through the - * resulting exception. - */ - fun from(cause: Throwable): FirebaseCommonAIException = - when (cause) { - is FirebaseCommonAIException -> cause - is JsonConvertException, - is kotlinx.serialization.SerializationException -> - SerializationException( - "Something went wrong while trying to deserialize a response from the server.", - cause, - ) - is TimeoutCancellationException -> - RequestTimeoutException("The request failed to complete in the allotted time.") - else -> UnknownException("Something unexpected happened.", cause) - } - } -} - -/** Something went wrong while trying to deserialize a response from the server. */ -internal class SerializationException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** The server responded with a non 200 response code. */ -internal class ServerException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** The server responded that the API Key is no valid. */ -internal class InvalidAPIKeyException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** - * A request was blocked for some reason. - * - * See the [response's][response] `promptFeedback.blockReason` for more information. - * - * @property response the full server response for the request. - */ -internal class PromptBlockedException -internal constructor( - val response: GenerateContentResponse.Internal?, - cause: Throwable? = null, - message: String? = null, -) : - FirebaseCommonAIException( - "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}", - cause, - ) { - internal constructor(message: String, cause: Throwable? = null) : this(null, cause, message) -} - -/** - * The user's location (region) is not supported by the API. - * - * See the Google documentation for a - * [list of regions](https://ai.google.dev/available_regions#available_regions) (countries and - * territories) where the API is available. - */ -internal class UnsupportedUserLocationException(cause: Throwable? = null) : - FirebaseCommonAIException("User location is not supported for the API use.", cause) - -/** - * Some form of state occurred that shouldn't have. - * - * Usually indicative of consumer error. - */ -internal class InvalidStateException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** - * A request was stopped during generation for some reason. - * - * @property response the full server response for the request - */ -internal class ResponseStoppedException( - val response: GenerateContentResponse.Internal, - cause: Throwable? = null -) : - FirebaseCommonAIException( - "Content generation stopped. Reason: ${response.candidates?.first()?.finishReason?.name}", - cause, - ) - -/** - * A request took too long to complete. - * - * Usually occurs due to a user specified [timeout][RequestOptions.timeout]. - */ -internal class RequestTimeoutException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** The quota for this API key is depleted, retry this request at a later time. */ -internal class QuotaExceededException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** The service is not enabled for this project. Visit the Firebase Console to enable it. */ -internal class ServiceDisabledException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -/** Catch all case for exceptions not explicitly expected. */ -internal class UnknownException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -internal class ContentBlockedException(message: String, cause: Throwable? = null) : - FirebaseCommonAIException(message, cause) - -internal fun makeMissingCaseException( - source: String, - ordinal: Int -): com.google.firebase.ai.type.SerializationException { - return com.google.firebase.ai.type.SerializationException( - """ - |Missing case for a $source: $ordinal - |This error indicates that one of the `toInternal` conversions needs updating. - |If you're a developer seeing this exception, please file an issue on our GitHub repo: - |https://github.com/firebase/firebase-android-sdk - """ - .trimMargin() - ) -} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt index ebc3db7f282..bb6bf242bb0 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt @@ -21,7 +21,9 @@ import com.google.firebase.ai.common.util.fullModelName import com.google.firebase.ai.common.util.trimmedModelName import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.ImagenEditingConfig import com.google.firebase.ai.type.ImagenImageFormat +import com.google.firebase.ai.type.ImagenReferenceImage import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.SafetySetting import com.google.firebase.ai.type.Tool @@ -75,17 +77,22 @@ internal data class CountTokensRequest( } @Serializable +@OptIn(PublicPreviewAPI::class) internal data class GenerateImageRequest( val instances: List, val parameters: ImagenParameters, ) : Request { - @Serializable internal data class ImagenPrompt(val prompt: String) + @Serializable + internal data class ImagenPrompt( + val prompt: String?, + val referenceImages: List? + ) - @OptIn(PublicPreviewAPI::class) @Serializable internal data class ImagenParameters( val sampleCount: Int, val includeRaiReason: Boolean, + val includeSafetyAttributes: Boolean, val storageUri: String?, val negativePrompt: String?, val aspectRatio: String?, @@ -93,5 +100,19 @@ internal data class GenerateImageRequest( val personGeneration: String?, val addWatermark: Boolean?, val imageOutputOptions: ImagenImageFormat.Internal?, + val editMode: String?, + @OptIn(PublicPreviewAPI::class) val editConfig: ImagenEditingConfig.Internal?, ) + + @Serializable + internal enum class ReferenceType { + @SerialName("REFERENCE_TYPE_UNSPECIFIED") UNSPECIFIED, + @SerialName("REFERENCE_TYPE_RAW") RAW, + @SerialName("REFERENCE_TYPE_MASK") MASK, + @SerialName("REFERENCE_TYPE_CONTROL") CONTROL, + @SerialName("REFERENCE_TYPE_STYLE") STYLE, + @SerialName("REFERENCE_TYPE_SUBJECT") SUBJECT, + @SerialName("REFERENCE_TYPE_MASKED_SUBJECT") MASKED_SUBJECT, + @SerialName("REFERENCE_TYPE_PRODUCT") PRODUCT + } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt index 4d7a1e46097..9f1bbd37260 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/android.kt @@ -17,8 +17,8 @@ package com.google.firebase.ai.common.util import android.media.AudioRecord +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.yield /** * The minimum buffer size for this instance. @@ -38,13 +38,17 @@ internal fun AudioRecord.readAsFlow() = flow { while (true) { if (recordingState != AudioRecord.RECORDSTATE_RECORDING) { - yield() + // delay uses a different scheduler in the backend, so it's "stickier" in its enforcement when + // compared to yield. + delay(0) continue } - val bytesRead = read(buffer, 0, buffer.size) if (bytesRead > 0) { emit(buffer.copyOf(bytesRead)) } + // delay uses a different scheduler in the backend, so it's "stickier" in its enforcement when + // compared to yield. + delay(0) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt index 91490da4126..db0a3745785 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/util/serialization.kt @@ -17,7 +17,7 @@ package com.google.firebase.ai.common.util import android.util.Log -import com.google.firebase.ai.common.SerializationException +import com.google.firebase.ai.type.SerializationException import kotlin.reflect.KClass import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt index 57a531c1cd8..51a90135e12 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt @@ -42,7 +42,8 @@ public abstract class GenerativeModelFutures internal constructor() { * @throws [FirebaseAIException] if the request failed. */ public abstract fun generateContent( - vararg prompt: Content + prompt: Content, + vararg prompts: Content ): ListenableFuture /** @@ -53,7 +54,8 @@ public abstract class GenerativeModelFutures internal constructor() { * @throws [FirebaseAIException] if the request failed. */ public abstract fun generateContentStream( - vararg prompt: Content + prompt: Content, + vararg prompts: Content ): Publisher /** @@ -63,7 +65,10 @@ public abstract class GenerativeModelFutures internal constructor() { * @return The [CountTokensResponse] of running the model's tokenizer on the input. * @throws [FirebaseAIException] if the request failed. */ - public abstract fun countTokens(vararg prompt: Content): ListenableFuture + public abstract fun countTokens( + prompt: Content, + vararg prompts: Content + ): ListenableFuture /** * Creates a [ChatFutures] instance which internally tracks the ongoing conversation with the @@ -83,15 +88,22 @@ public abstract class GenerativeModelFutures internal constructor() { private class FuturesImpl(private val model: GenerativeModel) : GenerativeModelFutures() { override fun generateContent( - vararg prompt: Content + prompt: Content, + vararg prompts: Content ): ListenableFuture = - SuspendToFutureAdapter.launchFuture { model.generateContent(*prompt) } - - override fun generateContentStream(vararg prompt: Content): Publisher = - model.generateContentStream(*prompt).asPublisher() - - override fun countTokens(vararg prompt: Content): ListenableFuture = - SuspendToFutureAdapter.launchFuture { model.countTokens(*prompt) } + SuspendToFutureAdapter.launchFuture { model.generateContent(prompt, *prompts) } + + override fun generateContentStream( + prompt: Content, + vararg prompts: Content + ): Publisher = + model.generateContentStream(prompt, *prompts).asPublisher() + + override fun countTokens( + prompt: Content, + vararg prompts: Content + ): ListenableFuture = + SuspendToFutureAdapter.launchFuture { model.countTokens(prompt, *prompts) } override fun startChat(): ChatFutures = startChat(emptyList()) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt index 99d42d32732..2f0299da406 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/ImagenModelFutures.kt @@ -19,8 +19,14 @@ package com.google.firebase.ai.java import androidx.concurrent.futures.SuspendToFutureAdapter import com.google.common.util.concurrent.ListenableFuture import com.google.firebase.ai.ImagenModel +import com.google.firebase.ai.type.Dimensions +import com.google.firebase.ai.type.ImagenEditMode +import com.google.firebase.ai.type.ImagenEditingConfig import com.google.firebase.ai.type.ImagenGenerationResponse +import com.google.firebase.ai.type.ImagenImagePlacement import com.google.firebase.ai.type.ImagenInlineImage +import com.google.firebase.ai.type.ImagenMaskReference +import com.google.firebase.ai.type.ImagenReferenceImage import com.google.firebase.ai.type.PublicPreviewAPI /** @@ -39,6 +45,69 @@ public abstract class ImagenModelFutures internal constructor() { prompt: String, ): ListenableFuture> + /** + * Generates an image from a single or set of base images, returning the result directly to the + * caller. + * + * @param prompt the text input given to the model as a prompt + * @param referenceImages the image inputs given to the model as a prompt + * @param config the editing configuration settings + */ + public abstract fun editImage( + referenceImages: List, + prompt: String, + config: ImagenEditingConfig? = null + ): ListenableFuture> + + /** + * Generates an image from a single or set of base images, returning the result directly to the + * caller. + * + * @param prompt the text input given to the model as a prompt + * @param referenceImages the image inputs given to the model as a prompt + */ + public abstract fun editImage( + referenceImages: List, + prompt: String, + ): ListenableFuture> + + /** + * Generates an image by inpainting a masked off part of a base image. + * + * @param image the base image + * @param prompt the text input given to the model as a prompt + * @param mask the mask which defines where in the image can be painted by imagen. + * @param config the editing configuration settings, it should include an [ImagenEditMode] + */ + public abstract fun inpaintImage( + image: ImagenInlineImage, + prompt: String, + mask: ImagenMaskReference, + config: ImagenEditingConfig, + ): ListenableFuture> + + /** + * Generates an image by outpainting the image, extending its borders + * + * @param image the base image + * @param newDimensions the new dimensions for the image, *must* be larger than the original + * image. + * @param newPosition the placement of the base image within the new image. This can either be + * coordinates (0,0 is the top left corner) or an alignment (ex: + * [ImagenImagePlacement.BOTTOM_CENTER]) + * @param prompt optional, but can be used to specify the background generated if context is + * insufficient + * @param config the editing configuration settings + * @see [ImagenMaskReference.generateMaskAndPadForOutpainting] + */ + public abstract fun outpaintImage( + image: ImagenInlineImage, + newDimensions: Dimensions, + newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER, + prompt: String = "", + config: ImagenEditingConfig? = null, + ): ListenableFuture> + /** Returns the [ImagenModel] object wrapped by this object. */ public abstract fun getImageModel(): ImagenModel @@ -48,6 +117,38 @@ public abstract class ImagenModelFutures internal constructor() { ): ListenableFuture> = SuspendToFutureAdapter.launchFuture { model.generateImages(prompt) } + override fun editImage( + referenceImages: List, + prompt: String, + config: ImagenEditingConfig? + ): ListenableFuture> = + SuspendToFutureAdapter.launchFuture { model.editImage(referenceImages, prompt, config) } + + override fun editImage( + referenceImages: List, + prompt: String, + ): ListenableFuture> = + editImage(referenceImages, prompt, null) + + override fun inpaintImage( + image: ImagenInlineImage, + prompt: String, + mask: ImagenMaskReference, + config: ImagenEditingConfig + ): ListenableFuture> = + SuspendToFutureAdapter.launchFuture { model.inpaintImage(image, prompt, mask, config) } + + override fun outpaintImage( + image: ImagenInlineImage, + newDimensions: Dimensions, + newPosition: ImagenImagePlacement, + prompt: String, + config: ImagenEditingConfig? + ): ListenableFuture> = + SuspendToFutureAdapter.launchFuture { + model.outpaintImage(image, newDimensions, newPosition, prompt, config) + } + override fun getImageModel(): ImagenModel = model } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt index 1efa2dfedfc..5a04ed9f97c 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/LiveSessionFutures.kt @@ -23,11 +23,13 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.FunctionCallPart import com.google.firebase.ai.type.FunctionResponsePart +import com.google.firebase.ai.type.InlineData import com.google.firebase.ai.type.LiveServerMessage import com.google.firebase.ai.type.LiveSession import com.google.firebase.ai.type.MediaData import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.SessionAlreadyReceivingException +import com.google.firebase.ai.type.Transcription import io.ktor.websocket.close import kotlinx.coroutines.reactive.asPublisher import org.reactivestreams.Publisher @@ -40,6 +42,13 @@ import org.reactivestreams.Publisher @PublicPreviewAPI public abstract class LiveSessionFutures internal constructor() { + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation]. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation(): ListenableFuture + /** * Starts an audio conversation with the model, which can only be stopped using * [stopAudioConversation] or [close]. @@ -47,6 +56,7 @@ public abstract class LiveSessionFutures internal constructor() { * @param functionCallHandler A callback function that is invoked whenever the model receives a * function call. */ + @RequiresPermission(RECORD_AUDIO) public abstract fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? ): ListenableFuture @@ -54,9 +64,90 @@ public abstract class LiveSessionFutures internal constructor() { /** * Starts an audio conversation with the model, which can only be stopped using * [stopAudioConversation]. + * @param transcriptHandler A callback function that is invoked whenever the model receives a + * transcript. The first [Transcription] object is the input transcription, and the second is the + * output transcription */ @RequiresPermission(RECORD_AUDIO) - public abstract fun startAudioConversation(): ListenableFuture + public abstract fun startAudioConversation( + transcriptHandler: ((Transcription?, Transcription?) -> Unit)?, + ): ListenableFuture + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation(enableInterruptions: Boolean): ListenableFuture + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param transcriptHandler A callback function that is invoked whenever the model receives a + * transcript. The first [Transcription] object is the input transcription, and the second is the + * output transcription + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation( + transcriptHandler: ((Transcription?, Transcription?) -> Unit)?, + enableInterruptions: Boolean + ): ListenableFuture + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)?, + enableInterruptions: Boolean + ): ListenableFuture + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. + * + * @param transcriptHandler A callback function that is invoked whenever the model receives a + * transcript. The first [Transcription] object is the input transcription, and the second is the + * output transcription + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public abstract fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)?, + transcriptHandler: ((Transcription?, Transcription?) -> Unit)?, + enableInterruptions: Boolean + ): ListenableFuture /** * Stops the audio conversation with the Gemini Server. @@ -93,6 +184,30 @@ public abstract class LiveSessionFutures internal constructor() { functionList: List ): ListenableFuture + /** + * Sends audio data to the server in realtime. Check + * https://ai.google.dev/api/live#bidigeneratecontentrealtimeinput for details about the realtime + * input usage. + * @param audio The audio data to send. + */ + public abstract fun sendAudioRealtime(audio: InlineData): ListenableFuture + + /** + * Sends video data to the server in realtime. Check + * https://ai.google.dev/api/live#bidigeneratecontentrealtimeinput for details about the realtime + * input usage. + * @param video The video data to send. Video MIME type could be either video or image. + */ + public abstract fun sendVideoRealtime(video: InlineData): ListenableFuture + + /** + * Sends text data to the server in realtime. Check + * https://ai.google.dev/api/live#bidigeneratecontentrealtimeinput for details about the realtime + * input usage. + * @param text The text data to send. + */ + public abstract fun sendTextRealtime(text: String): ListenableFuture + /** * Streams client data to the model. * @@ -100,6 +215,7 @@ public abstract class LiveSessionFutures internal constructor() { * * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. */ + @Deprecated("Use sendAudioRealtime, sendVideoRealtime, or sendTextRealtime instead") public abstract fun sendMediaStream(mediaChunks: List): ListenableFuture /** @@ -157,6 +273,15 @@ public abstract class LiveSessionFutures internal constructor() { override fun sendFunctionResponse(functionList: List) = SuspendToFutureAdapter.launchFuture { session.sendFunctionResponse(functionList) } + override fun sendAudioRealtime(audio: InlineData): ListenableFuture = + SuspendToFutureAdapter.launchFuture { session.sendAudioRealtime(audio) } + + override fun sendVideoRealtime(video: InlineData): ListenableFuture = + SuspendToFutureAdapter.launchFuture { session.sendVideoRealtime(video) } + + override fun sendTextRealtime(text: String): ListenableFuture = + SuspendToFutureAdapter.launchFuture { session.sendTextRealtime(text) } + override fun sendMediaStream(mediaChunks: List) = SuspendToFutureAdapter.launchFuture { session.sendMediaStream(mediaChunks) } @@ -165,10 +290,62 @@ public abstract class LiveSessionFutures internal constructor() { functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? ) = SuspendToFutureAdapter.launchFuture { session.startAudioConversation(functionCallHandler) } + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation( + transcriptHandler: ((Transcription?, Transcription?) -> Unit)? + ) = + SuspendToFutureAdapter.launchFuture { + session.startAudioConversation(transcriptHandler = transcriptHandler) + } + @RequiresPermission(RECORD_AUDIO) override fun startAudioConversation() = SuspendToFutureAdapter.launchFuture { session.startAudioConversation() } + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation(enableInterruptions: Boolean) = + SuspendToFutureAdapter.launchFuture { + session.startAudioConversation(enableInterruptions = enableInterruptions) + } + + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation( + transcriptHandler: ((Transcription?, Transcription?) -> Unit)?, + enableInterruptions: Boolean + ) = + SuspendToFutureAdapter.launchFuture { + session.startAudioConversation( + transcriptHandler = transcriptHandler, + enableInterruptions = enableInterruptions + ) + } + + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)?, + transcriptHandler: ((Transcription?, Transcription?) -> Unit)?, + enableInterruptions: Boolean + ) = + SuspendToFutureAdapter.launchFuture { + session.startAudioConversation( + functionCallHandler = functionCallHandler, + transcriptHandler = transcriptHandler, + enableInterruptions = enableInterruptions + ) + } + + @RequiresPermission(RECORD_AUDIO) + override fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)?, + enableInterruptions: Boolean + ) = + SuspendToFutureAdapter.launchFuture { + session.startAudioConversation( + functionCallHandler, + enableInterruptions = enableInterruptions + ) + } + override fun stopAudioConversation() = SuspendToFutureAdapter.launchFuture { session.stopAudioConversation() } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt index 4db66ae6c3e..06b4a3efe25 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioHelper.kt @@ -141,7 +141,6 @@ internal class AudioHelper( */ fun listenToRecording(): Flow { if (released) return emptyFlow() - resumeRecording() return recorder.readAsFlow() @@ -163,7 +162,10 @@ internal class AudioHelper( fun build(): AudioHelper { val playbackTrack = AudioTrack( - AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build(), + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build(), AudioFormat.Builder() .setSampleRate(24000) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioTranscriptionConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioTranscriptionConfig.kt new file mode 100644 index 00000000000..406af4d4c6f --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/AudioTranscriptionConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +/** The audio transcription configuration. Its presence enables audio transcription */ +public class AudioTranscriptionConfig { + + @Serializable internal object Internal + + internal fun toInternal() = Internal +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt index d5fc51f21c0..5b0c57ce61a 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Candidate.kt @@ -33,62 +33,48 @@ import kotlinx.serialization.json.JsonNames * @property safetyRatings A list of [SafetyRating]s describing the generated content. * @property citationMetadata Metadata about the sources used to generate this content. * @property finishReason The reason the model stopped generating content, if it exist. + * @property groundingMetadata Metadata returned to the client when the model grounds its response. + * @property urlContextMetadata Metadata returned to the client when the [UrlContext] tool is + * enabled. */ public class Candidate +@OptIn(PublicPreviewAPI::class) internal constructor( public val content: Content, public val safetyRatings: List, public val citationMetadata: CitationMetadata?, - public val finishReason: FinishReason? + public val finishReason: FinishReason?, + public val groundingMetadata: GroundingMetadata?, + @property:PublicPreviewAPI public val urlContextMetadata: UrlContextMetadata? ) { + @OptIn(PublicPreviewAPI::class) @Serializable internal data class Internal( val content: Content.Internal? = null, val finishReason: FinishReason.Internal? = null, val safetyRatings: List? = null, val citationMetadata: CitationMetadata.Internal? = null, - val groundingMetadata: GroundingMetadata? = null, + val groundingMetadata: GroundingMetadata.Internal? = null, + val urlContextMetadata: UrlContextMetadata.Internal? = null ) { + + @OptIn(PublicPreviewAPI::class) internal fun toPublic(): Candidate { val safetyRatings = safetyRatings?.mapNotNull { it.toPublic() }.orEmpty() val citations = citationMetadata?.toPublic() val finishReason = finishReason?.toPublic() + val groundingMetadata = groundingMetadata?.toPublic() + val urlContextMetadata = urlContextMetadata?.toPublic() return Candidate( this.content?.toPublic() ?: content("model") {}, safetyRatings, citations, - finishReason - ) - } - - @Serializable - internal data class GroundingMetadata( - @SerialName("web_search_queries") val webSearchQueries: List?, - @SerialName("search_entry_point") val searchEntryPoint: SearchEntryPoint?, - @SerialName("retrieval_queries") val retrievalQueries: List?, - @SerialName("grounding_attribution") val groundingAttribution: List?, - ) { - - @Serializable - internal data class SearchEntryPoint( - @SerialName("rendered_content") val renderedContent: String?, - @SerialName("sdk_blob") val sdkBlob: String?, + finishReason, + groundingMetadata, + urlContextMetadata ) - - @Serializable - internal data class GroundingAttribution( - val segment: Segment, - @SerialName("confidence_score") val confidenceScore: Float?, - ) { - - @Serializable - internal data class Segment( - @SerialName("start_index") val startIndex: Int, - @SerialName("end_index") val endIndex: Int, - ) - } } } } @@ -100,8 +86,8 @@ internal constructor( * * @property category The category of harm being assessed (e.g., Hate speech). * @property probability The likelihood of the content causing harm. - * @property probabilityScore A numerical score representing the probability of harm, between 0 and - * 1. + * @property probabilityScore A numerical score representing the probability of harm, between `0` + * and `1`. * @property blocked Indicates whether the content was blocked due to safety concerns. * @property severity The severity of the potential harm. * @property severityScore A numerical score representing the severity of harm. @@ -317,3 +303,285 @@ public class FinishReason private constructor(public val name: String, public va public val MALFORMED_FUNCTION_CALL: FinishReason = FinishReason("MALFORMED_FUNCTION_CALL", 9) } } + +/** + * Metadata returned to the client when grounding is enabled. + * + * If using grounding with Google Search, you are required to comply with the "Grounding with Google + * Search" usage requirements for your chosen API provider: + * [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or + * Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section + * within the Service Specific Terms). + * + * @property webSearchQueries The list of web search queries that the model performed to gather the + * grounding information. These can be used to allow users to explore the search results themselves. + * @property searchEntryPoint Google Search entry point for web searches. This contains an HTML/CSS + * snippet that **must** be embedded in an app to display a Google Search Entry point for follow-up + * web searches related to the model's "Grounded Response". + * @property groundingChunks The list of [GroundingChunk] classes. Each chunk represents a piece of + * retrieved content that the model used to ground its response. + * @property groundingSupports The list of [GroundingSupport] objects. Each object details how + * specific segments of the model's response are supported by the `groundingChunks`. + */ +public class GroundingMetadata( + public val webSearchQueries: List, + public val searchEntryPoint: SearchEntryPoint?, + public val retrievalQueries: List, + @Deprecated("Use groundingChunks instead") + public val groundingAttribution: List, + public val groundingChunks: List, + public val groundingSupports: List, +) { + @Serializable + internal data class Internal( + val webSearchQueries: List?, + val searchEntryPoint: SearchEntryPoint.Internal?, + val retrievalQueries: List?, + @Deprecated("Use groundingChunks instead") + val groundingAttribution: List?, + val groundingChunks: List?, + val groundingSupports: List?, + ) { + internal fun toPublic() = + GroundingMetadata( + webSearchQueries = webSearchQueries.orEmpty(), + searchEntryPoint = searchEntryPoint?.toPublic(), + retrievalQueries = retrievalQueries.orEmpty(), + groundingAttribution = groundingAttribution?.map { it.toPublic() }.orEmpty(), + groundingChunks = groundingChunks?.map { it.toPublic() }.orEmpty(), + groundingSupports = groundingSupports?.map { it.toPublic() }.orEmpty().filterNotNull() + ) + } +} + +/** + * Represents a Google Search entry point. + * + * @property renderedContent An HTML/CSS snippet that can be embedded in your app. To ensure proper + * rendering, it's recommended to display this content within a `WebView`. + * @property sdkBlob A blob of data for the client SDK to render the search entry point. + */ +public class SearchEntryPoint( + public val renderedContent: String, + public val sdkBlob: String?, +) { + @Serializable + internal data class Internal( + val renderedContent: String?, + val sdkBlob: String?, + ) { + internal fun toPublic(): SearchEntryPoint { + // If rendered content is null, the user must not display the grounded result. If they do, + // they violate the service terms. To prevent this from happening, throw an exception. + if (renderedContent == null) { + throw SerializationException("renderedContent is null, should be a string") + } + return SearchEntryPoint(renderedContent = renderedContent, sdkBlob = sdkBlob) + } + } +} + +/** + * Represents a chunk of retrieved data that supports a claim in the model's response. + * + * @property web Contains details if the grounding chunk is from a web source. + */ +public class GroundingChunk( + public val web: WebGroundingChunk?, +) { + @Serializable + internal data class Internal( + val web: WebGroundingChunk.Internal?, + ) { + internal fun toPublic() = GroundingChunk(web = web?.toPublic()) + } +} + +/** + * A grounding chunk from the web. + * + * @property uri The URI of the retrieved web page. + * @property title The title of the retrieved web page. + * @property domain The domain of the original URI from which the content was retrieved. This is + * only populated when using the Vertex AI Gemini API. + */ +public class WebGroundingChunk( + public val uri: String?, + public val title: String?, + public val domain: String? +) { + @Serializable + internal data class Internal(val uri: String?, val title: String?, val domain: String?) { + internal fun toPublic() = WebGroundingChunk(uri = uri, title = title, domain = domain) + } +} + +/** + * Provides information about how a specific segment of the model's response is supported by the + * retrieved grounding chunks. + * + * @property segment Specifies the segment of the model's response content that this grounding + * support pertains to. + * @property groundingChunkIndices A list of indices that refer to specific [GroundingChunk] classes + * within the [GroundingMetadata.groundingChunks] array. These referenced chunks are the sources + * that support the claim made in the associated `segment` of the response. For example, an array + * `[1, 3, 4]` means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the + * retrieved content supporting this part of the response. + */ +public class GroundingSupport( + public val segment: Segment, + public val groundingChunkIndices: List, +) { + @Serializable + internal data class Internal( + val segment: Segment.Internal?, + val groundingChunkIndices: List?, + ) { + internal fun toPublic(): GroundingSupport? { + if (segment == null) { + return null + } + return GroundingSupport( + segment = segment.toPublic(), + groundingChunkIndices = groundingChunkIndices.orEmpty(), + ) + } + } +} + +@Deprecated("Use GroundingChunk instead") +public class GroundingAttribution( + public val segment: Segment, + public val confidenceScore: Float?, +) { + @Deprecated("Use GroundingChunk instead") + @Serializable + internal data class Internal( + val segment: Segment.Internal, + val confidenceScore: Float?, + ) { + internal fun toPublic() = + GroundingAttribution(segment = segment.toPublic(), confidenceScore = confidenceScore) + } +} + +/** + * Represents a specific segment within a [Content] object, often used to pinpoint the exact + * location of text or data that grounding information refers to. + * + * @property partIndex The zero-based index of the [Part] object within the `parts` array of its + * parent [Content] object. This identifies which part of the content the segment belongs to. + * @property startIndex The zero-based start index of the segment within the specified [Part], + * measured in UTF-8 bytes. This offset is inclusive, starting from 0 at the beginning of the part's + * content. + * @property endIndex The zero-based end index of the segment within the specified [Part], measured + * in UTF-8 bytes. This offset is exclusive, meaning the character at this index is not included in + * the segment. + * @property text The text corresponding to the segment from the response. + */ +public class Segment( + public val startIndex: Int, + public val endIndex: Int, + public val partIndex: Int, + public val text: String, +) { + @Serializable + internal data class Internal( + val startIndex: Int?, + val endIndex: Int?, + val partIndex: Int?, + val text: String?, + ) { + internal fun toPublic() = + Segment( + startIndex = startIndex ?: 0, + endIndex = endIndex ?: 0, + partIndex = partIndex ?: 0, + text = text ?: "" + ) + } +} + +/** + * Metadata related to the [UrlContext] tool. + * + * @property urlMetadata List of [UrlMetadata] used to provide context to the Gemini model. + */ +@PublicPreviewAPI +public class UrlContextMetadata internal constructor(public val urlMetadata: List) { + + @Serializable + @PublicPreviewAPI + internal data class Internal(val urlMetadata: List?) { + internal fun toPublic() = UrlContextMetadata(urlMetadata?.map { it.toPublic() } ?: emptyList()) + } +} + +/** + * Metadata for a single URL retrieved by the [UrlContext] tool. + * + * @property retrievedUrl The retrieved URL. + * @property urlRetrievalStatus The status of the URL retrieval. + */ +@PublicPreviewAPI +public class UrlMetadata +internal constructor( + public val retrievedUrl: String?, + public val urlRetrievalStatus: UrlRetrievalStatus +) { + @Serializable + internal data class Internal( + val retrievedUrl: String?, + val urlRetrievalStatus: UrlRetrievalStatus.Internal + ) { + internal fun toPublic() = UrlMetadata(retrievedUrl, urlRetrievalStatus.toPublic()) + } +} + +/** + * The status of a URL retrieval. + * + * @property name The name of the retrieval status. + * @property ordinal The ordinal value of the retrieval status. + */ +@PublicPreviewAPI +public class UrlRetrievalStatus +private constructor(public val name: String, public val ordinal: Int) { + + @Serializable(Internal.Serializer::class) + internal enum class Internal { + @SerialName("URL_RETRIEVAL_STATUS_UNSPECIFIED") UNSPECIFIED, + @SerialName("URL_RETRIEVAL_STATUS_SUCCESS") SUCCESS, + @SerialName("URL_RETRIEVAL_STATUS_ERROR") ERROR, + @SerialName("URL_RETRIEVAL_STATUS_PAYWALL") PAYWALL, + @SerialName("URL_RETRIEVAL_STATUS_UNSAFE") UNSAFE; + + internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) + + internal fun toPublic() = + when (this) { + SUCCESS -> UrlRetrievalStatus.SUCCESS + ERROR -> UrlRetrievalStatus.ERROR + PAYWALL -> UrlRetrievalStatus.PAYWALL + UNSAFE -> UrlRetrievalStatus.UNSAFE + else -> UrlRetrievalStatus.UNSPECIFIED + } + } + + public companion object { + /** Unspecified retrieval status. */ + @JvmField public val UNSPECIFIED: UrlRetrievalStatus = UrlRetrievalStatus("UNSPECIFIED", 0) + + /** The URL retrieval was successful. */ + @JvmField public val SUCCESS: UrlRetrievalStatus = UrlRetrievalStatus("SUCCESS", 1) + + /** The URL retrieval failed. */ + @JvmField public val ERROR: UrlRetrievalStatus = UrlRetrievalStatus("ERROR", 2) + + /** The URL retrieval failed because the content is behind a paywall. */ + @JvmField public val PAYWALL: UrlRetrievalStatus = UrlRetrievalStatus("PAYWALL", 3) + + /** The URL retrieval failed because the content is unsafe. */ + @JvmField public val UNSAFE: UrlRetrievalStatus = UrlRetrievalStatus("UNSAFE", 4) + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt index 4e9f1a860db..350d46e9063 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Content.kt @@ -17,6 +17,7 @@ package com.google.firebase.ai.type import android.graphics.Bitmap +import kotlin.collections.filterNot import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -90,14 +91,21 @@ constructor(public val role: String? = "user", public val parts: List) { @Serializable internal data class Internal( @EncodeDefault val role: String? = "user", - val parts: List + val parts: List? = null ) { internal fun toPublic(): Content { + // Return empty if none of the parts is a known part + if (parts == null || parts.filterNot { it is UnknownPart.Internal }.isEmpty()) { + return Content(role, emptyList()) + } + // From all the known parts, if they are all text and empty, we coalesce them into a single + // one-character string part so the backend doesn't fail if we send this back as part of a + // multi-turn interaction. val returnedParts = - parts.map { it.toPublic() }.filterNot { it is TextPart && it.text.isEmpty() } - // If all returned parts were text and empty, we coalesce them into a single one-character - // string - // part so the backend doesn't fail if we send this back as part of a multi-turn interaction. + parts + .filterNot { it is UnknownPart.Internal } + .map { it.toPublic() } + .filterNot { it is TextPart && it.text.isEmpty() } return Content(role, returnedParts.ifEmpty { listOf(TextPart(" ")) }) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt index 955f7bf941a..ac485a2c0d9 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/CountTokensResponse.kt @@ -22,19 +22,19 @@ import kotlinx.serialization.Serializable * The model's response to a count tokens request. * * **Important:** The counters in this class do not include billable image, video or other non-text - * input. See [Vertex AI pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing) for - * details. + * input. See [Pricing](https://firebase.google.com/docs/ai-logic/pricing) for details. * * @property totalTokens The total number of tokens in the input given to the model as a prompt. * @property totalBillableCharacters The total number of billable characters in the text input given * to the model as a prompt. **Important:** this property does not include billable image, video or - * other non-text input. See - * [Vertex AI pricing](https://cloud.google.com/vertex-ai/generative-ai/pricing) for details. + * other non-text input. See [Pricing](https://firebase.google.com/docs/ai-logic/pricing) for + * details. * @property promptTokensDetails The breakdown, by modality, of how many tokens are consumed by the * prompt. */ public class CountTokensResponse( public val totalTokens: Int, + @Deprecated("This field is deprecated and will be removed in a future version.") public val totalBillableCharacters: Int? = null, public val promptTokensDetails: List = emptyList(), ) { @@ -46,14 +46,14 @@ public class CountTokensResponse( @Serializable internal data class Internal( - val totalTokens: Int, + val totalTokens: Int? = null, val totalBillableCharacters: Int? = null, val promptTokensDetails: List? = null ) : Response { internal fun toPublic(): CountTokensResponse { return CountTokensResponse( - totalTokens, + totalTokens ?: 0, totalBillableCharacters ?: 0, promptTokensDetails?.map { it.toPublic() } ?: emptyList() ) diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/util.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Dimensions.kt similarity index 63% rename from firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/util.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Dimensions.kt index 97b9ffe555e..98f256f39b2 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/common/util/util.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Dimensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.google.firebase.vertexai.common.util +package com.google.firebase.ai.type /** - * Ensures the model name provided has a `models/` prefix - * - * Models must be prepended with the `models/` prefix when communicating with the backend. + * Represents the dimensions of an image in pixels + * @param width the width of the image in pixels + * @param height the height of the image in pixels */ -internal fun fullModelName(name: String): String = - name.takeIf { it.contains("/") } ?: "models/$name" +public class Dimensions(public val width: Int, public val height: Int) {} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt index 57a27f241a0..fed7660d08e 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt @@ -17,7 +17,6 @@ package com.google.firebase.ai.type import com.google.firebase.ai.FirebaseAI -import com.google.firebase.ai.common.FirebaseCommonAIException import kotlinx.coroutines.TimeoutCancellationException /** Parent class for any errors that occur from the [FirebaseAI] SDK. */ @@ -35,34 +34,6 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti internal fun from(cause: Throwable): FirebaseAIException = when (cause) { is FirebaseAIException -> cause - is FirebaseCommonAIException -> - when (cause) { - is com.google.firebase.ai.common.SerializationException -> - SerializationException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.ServerException -> - ServerException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.InvalidAPIKeyException -> - InvalidAPIKeyException(cause.message ?: "") - is com.google.firebase.ai.common.PromptBlockedException -> - PromptBlockedException(cause.response?.toPublic(), cause.cause) - is com.google.firebase.ai.common.UnsupportedUserLocationException -> - UnsupportedUserLocationException(cause.cause) - is com.google.firebase.ai.common.InvalidStateException -> - InvalidStateException(cause.message ?: "", cause) - is com.google.firebase.ai.common.ResponseStoppedException -> - ResponseStoppedException(cause.response.toPublic(), cause.cause) - is com.google.firebase.ai.common.RequestTimeoutException -> - RequestTimeoutException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.ServiceDisabledException -> - ServiceDisabledException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.UnknownException -> - UnknownException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.ContentBlockedException -> - ContentBlockedException(cause.message ?: "", cause.cause) - is com.google.firebase.ai.common.QuotaExceededException -> - QuotaExceededException(cause.message ?: "", cause.cause) - else -> UnknownException(cause.message ?: "", cause) - } is TimeoutCancellationException -> RequestTimeoutException("The request failed to complete in the allotted time.") else -> UnknownException("Something unexpected happened.", cause) @@ -149,6 +120,16 @@ internal constructor(message: String, cause: Throwable? = null) : public class UnsupportedUserLocationException internal constructor(cause: Throwable? = null) : FirebaseAIException("User location is not supported for the API use.", cause) +/** + * The Firebase project has not been configured and enabled for the selected API. + * + * For the Gemini Developer API, see + * [steps](https://firebase.google.com/docs/ai-logic/faq-and-troubleshooting?api=dev#error-genai-config-not-found) + */ +public class APINotConfiguredException +internal constructor(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + /** * Some form of state occurred that shouldn't have. * @@ -219,6 +200,25 @@ public class AudioRecordInitializationFailedException(message: String) : public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) : FirebaseAIException(message, cause) +/** The request is missing a permission that is required to perform the requested operation. */ +public class PermissionMissingException(message: String, cause: Throwable? = null) : + FirebaseAIException(message, cause) + /** Catch all case for exceptions not explicitly expected. */ public class UnknownException internal constructor(message: String, cause: Throwable? = null) : FirebaseAIException(message, cause) + +internal fun makeMissingCaseException( + source: String, + ordinal: Int +): com.google.firebase.ai.type.SerializationException { + return com.google.firebase.ai.type.SerializationException( + """ + |Missing case for a $source: $ordinal + |This error indicates that one of the `toInternal` conversions needs updating. + |If you're a developer seeing this exception, please file an issue on our GitHub repo: + |https://github.com/firebase/firebase-android-sdk + """ + .trimMargin() + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt index 2b73d5ccfb1..65b753efda7 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/FunctionDeclaration.kt @@ -61,12 +61,12 @@ public class FunctionDeclaration( internal val schema: Schema = Schema.obj(properties = parameters, optionalProperties = optionalParameters, nullable = false) - internal fun toInternal() = Internal(name, description, schema.toInternal()) + internal fun toInternal() = Internal(name, description, schema.toInternalOpenApi()) @Serializable internal data class Internal( val name: String, val description: String, - val parameters: Schema.Internal + val parameters: Schema.InternalOpenAPI ) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt index be2b50f3be4..bbf5fc0ff73 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerateContentResponse.kt @@ -32,30 +32,68 @@ public class GenerateContentResponse( public val usageMetadata: UsageMetadata?, ) { /** - * Convenience field representing all the text parts in the response as a single string, if they - * exists. + * Convenience field representing all the text parts in the response as a single string. + * + * The value is null if the response contains no valid text [candidates]. + * + * Any part that's marked as a thought will be ignored. Learn more about + * [thinking](https://firebase.google.com/docs/ai-logic/thinking?api=dev). */ public val text: String? by lazy { - candidates.first().content.parts.filterIsInstance().joinToString(" ") { it.text } + val parts = candidates.firstOrNull()?.nonThoughtParts()?.filterIsInstance() + if (parts.isNullOrEmpty()) return@lazy null + parts.joinToString(" ") { it.text } } - /** Convenience field to list all the [FunctionCallPart]s in the response, if they exist. */ + /** + * Convenience field to list all the [FunctionCallPart]s in the response. + * + * The value is an empty list if the response contains no [candidates]. + * + * Any part that's marked as a thought will be ignored. Learn more about + * [thinking](https://firebase.google.com/docs/ai-logic/thinking?api=dev). + */ public val functionCalls: List by lazy { - candidates.first().content.parts.filterIsInstance() + candidates.firstOrNull()?.nonThoughtParts()?.filterIsInstance().orEmpty() } /** - * Convenience field representing all the [InlineDataPart]s in the first candidate, if they exist. + * Convenience field representing all the text parts in the response that are marked as thoughts + * as a single string, if they exist. + * + * Learn more about [thinking](https://firebase.google.com/docs/ai-logic/thinking?api=dev). + */ + public val thoughtSummary: String? by lazy { + candidates.firstOrNull()?.thoughtParts()?.filterIsInstance()?.joinToString(" ") { + it.text + } + } + + /** + * Convenience field representing all the [InlineDataPart]s in the first candidate. * * This also includes any [ImagePart], but they will be represented as [InlineDataPart] instead. + * + * The value is an empty list if the response contains no [candidates]. + * + * Any part that's marked as a thought will be ignored. Learn more about + * [thinking](https://firebase.google.com/docs/ai-logic/thinking?api=dev). */ public val inlineDataParts: List by lazy { - candidates.first().content.parts.let { parts -> - parts.filterIsInstance().map { it.toInlineDataPart() } + - parts.filterIsInstance() - } + candidates + .firstOrNull() + ?.nonThoughtParts() + ?.let { parts -> + parts.filterIsInstance().map { it.toInlineDataPart() } + + parts.filterIsInstance() + } + .orEmpty() } + private fun Candidate.thoughtParts(): List = content.parts.filter { it.isThought } + + private fun Candidate.nonThoughtParts(): List = content.parts.filter { !it.isThought } + @Serializable internal data class Internal( val candidates: List? = null, diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt index 1c2d2680bb1..a496098787f 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerationConfig.kt @@ -91,6 +91,7 @@ private constructor( internal val responseMimeType: String?, internal val responseSchema: Schema?, internal val responseModalities: List?, + internal val thinkingConfig: ThinkingConfig?, ) { /** @@ -135,6 +136,7 @@ private constructor( @JvmField public var responseMimeType: String? = null @JvmField public var responseSchema: Schema? = null @JvmField public var responseModalities: List? = null + @JvmField public var thinkingConfig: ThinkingConfig? = null public fun setTemperature(temperature: Float?): Builder = apply { this.temperature = temperature @@ -165,6 +167,9 @@ private constructor( public fun setResponseModalities(responseModalities: List?): Builder = apply { this.responseModalities = responseModalities } + public fun setThinkingConfig(thinkingConfig: ThinkingConfig?): Builder = apply { + this.thinkingConfig = thinkingConfig + } /** Create a new [GenerationConfig] with the attached arguments. */ public fun build(): GenerationConfig = @@ -179,7 +184,8 @@ private constructor( frequencyPenalty = frequencyPenalty, responseMimeType = responseMimeType, responseSchema = responseSchema, - responseModalities = responseModalities + responseModalities = responseModalities, + thinkingConfig = thinkingConfig ) } @@ -194,8 +200,9 @@ private constructor( frequencyPenalty = frequencyPenalty, presencePenalty = presencePenalty, responseMimeType = responseMimeType, - responseSchema = responseSchema?.toInternal(), - responseModalities = responseModalities?.map { it.toInternal() } + responseSchema = responseSchema?.toInternalOpenApi(), + responseModalities = responseModalities?.map { it.toInternal() }, + thinkingConfig = thinkingConfig?.toInternal() ) @Serializable @@ -209,8 +216,9 @@ private constructor( @SerialName("response_mime_type") val responseMimeType: String? = null, @SerialName("presence_penalty") val presencePenalty: Float? = null, @SerialName("frequency_penalty") val frequencyPenalty: Float? = null, - @SerialName("response_schema") val responseSchema: Schema.Internal? = null, - @SerialName("response_modalities") val responseModalities: List? = null + @SerialName("response_schema") val responseSchema: Schema.InternalOpenAPI? = null, + @SerialName("response_modalities") val responseModalities: List? = null, + @SerialName("thinking_config") val thinkingConfig: ThinkingConfig.Internal? = null ) public companion object { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt index 9598266fa68..29af741a21e 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt @@ -27,7 +27,7 @@ internal constructor(internal val location: String, internal val backend: Genera GenerativeBackend("", GenerativeBackendEnum.GOOGLE_AI) /** - * References the VertexAI Enterprise backend. + * References the VertexAI Gemini API backend. * * @param location passes a valid cloud server location, defaults to "us-central1" */ diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GoogleSearch.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GoogleSearch.kt new file mode 100644 index 00000000000..b192c2d33f5 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GoogleSearch.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +/** + * A tool that allows the generative model to connect to Google Search to access and incorporate + * up-to-date information from the web into its responses. + * + * When using this feature, you are required to comply with the "grounding with Google Search" usage + * requirements for your chosen API provider: + * [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) or + * Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) section + * within the Service Specific Terms). + */ +public class GoogleSearch { + @Serializable internal class Internal() + + internal fun toInternal() = Internal() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt index a65d153bc53..c3b2e6c2fce 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockMethod.kt @@ -16,7 +16,6 @@ package com.google.firebase.ai.type -import com.google.firebase.ai.common.makeMissingCaseException import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt index 93ebfde5da9..5a68e2d44e5 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmBlockThreshold.kt @@ -16,7 +16,6 @@ package com.google.firebase.ai.type -import com.google.firebase.ai.common.makeMissingCaseException import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt index 5144058ab44..ef97348b27c 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/HarmCategory.kt @@ -16,7 +16,6 @@ package com.google.firebase.ai.type -import com.google.firebase.ai.common.makeMissingCaseException import com.google.firebase.ai.common.util.FirstOrdinalSerializer import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName @@ -31,6 +30,10 @@ public class HarmCategory private constructor(public val ordinal: Int) { SEXUALLY_EXPLICIT -> Internal.SEXUALLY_EXPLICIT DANGEROUS_CONTENT -> Internal.DANGEROUS_CONTENT CIVIC_INTEGRITY -> Internal.CIVIC_INTEGRITY + IMAGE_HATE -> Internal.IMAGE_HATE + IMAGE_DANGEROUS_CONTENT -> Internal.IMAGE_DANGEROUS_CONTENT + IMAGE_HARASSMENT -> Internal.IMAGE_HARASSMENT + IMAGE_SEXUALLY_EXPLICIT -> Internal.IMAGE_SEXUALLY_EXPLICIT UNKNOWN -> Internal.UNKNOWN else -> throw makeMissingCaseException("HarmCategory", ordinal) } @@ -41,7 +44,11 @@ public class HarmCategory private constructor(public val ordinal: Int) { @SerialName("HARM_CATEGORY_HATE_SPEECH") HATE_SPEECH, @SerialName("HARM_CATEGORY_SEXUALLY_EXPLICIT") SEXUALLY_EXPLICIT, @SerialName("HARM_CATEGORY_DANGEROUS_CONTENT") DANGEROUS_CONTENT, - @SerialName("HARM_CATEGORY_CIVIC_INTEGRITY") CIVIC_INTEGRITY; + @SerialName("HARM_CATEGORY_CIVIC_INTEGRITY") CIVIC_INTEGRITY, + @SerialName("HARM_CATEGORY_IMAGE_HATE") IMAGE_HATE, + @SerialName("HARM_CATEGORY_IMAGE_DANGEROUS_CONTENT") IMAGE_DANGEROUS_CONTENT, + @SerialName("HARM_CATEGORY_IMAGE_HARASSMENT") IMAGE_HARASSMENT, + @SerialName("HARM_CATEGORY_IMAGE_SEXUALLY_EXPLICIT") IMAGE_SEXUALLY_EXPLICIT; internal object Serializer : KSerializer by FirstOrdinalSerializer(Internal::class) @@ -52,6 +59,10 @@ public class HarmCategory private constructor(public val ordinal: Int) { SEXUALLY_EXPLICIT -> HarmCategory.SEXUALLY_EXPLICIT DANGEROUS_CONTENT -> HarmCategory.DANGEROUS_CONTENT CIVIC_INTEGRITY -> HarmCategory.CIVIC_INTEGRITY + IMAGE_HATE -> HarmCategory.IMAGE_HATE + IMAGE_DANGEROUS_CONTENT -> HarmCategory.IMAGE_DANGEROUS_CONTENT + IMAGE_HARASSMENT -> HarmCategory.IMAGE_HARASSMENT + IMAGE_SEXUALLY_EXPLICIT -> HarmCategory.IMAGE_SEXUALLY_EXPLICIT else -> HarmCategory.UNKNOWN } } @@ -59,19 +70,34 @@ public class HarmCategory private constructor(public val ordinal: Int) { /** A new and not yet supported value. */ @JvmField public val UNKNOWN: HarmCategory = HarmCategory(0) - /** Harassment content. */ + /** Represents the harm category for content that is classified as harassment. */ @JvmField public val HARASSMENT: HarmCategory = HarmCategory(1) - /** Hate speech and content. */ + /** Represents the harm category for content that is classified as hate speech. */ @JvmField public val HATE_SPEECH: HarmCategory = HarmCategory(2) - /** Sexually explicit content. */ + /** Represents the harm category for content that is classified as sexually explicit content. */ @JvmField public val SEXUALLY_EXPLICIT: HarmCategory = HarmCategory(3) - /** Dangerous content. */ + /** Represents the harm category for content that is classified as dangerous content. */ @JvmField public val DANGEROUS_CONTENT: HarmCategory = HarmCategory(4) - /** Content that may be used to harm civic integrity. */ + /** + * Represents the harm category for content that is classified as content that may be used to + * harm civic integrity. + */ @JvmField public val CIVIC_INTEGRITY: HarmCategory = HarmCategory(5) + + /** Represents the harm category for image content that is classified as hateful. */ + @JvmField public val IMAGE_HATE: HarmCategory = HarmCategory(6) + + /** Represents the harm category for image content that is classified as dangerous. */ + @JvmField public val IMAGE_DANGEROUS_CONTENT: HarmCategory = HarmCategory(7) + + /** Represents the harm category for image content that is classified as harassment. */ + @JvmField public val IMAGE_HARASSMENT: HarmCategory = HarmCategory(8) + + /** Represents the harm category for image content that is classified as sexually explicit. */ + @JvmField public val IMAGE_SEXUALLY_EXPLICIT: HarmCategory = HarmCategory(9) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt index 10a8b4fed84..a5d5e22fca3 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenAspectRatio.kt @@ -17,7 +17,6 @@ package com.google.firebase.ai.type /** Represents the aspect ratio that the generated image should conform to. */ -@PublicPreviewAPI public class ImagenAspectRatio private constructor(internal val internalVal: String) { public companion object { /** A square image, useful for icons, profile pictures, etc. */ diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlConfig.kt new file mode 100644 index 00000000000..c6f3b01af6c --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlConfig.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +internal class ImagenControlConfig( + internal val controlType: ImagenControlType, + internal val enableComputation: Boolean? = null, + internal val superpixelRegionSize: Int? = null, + internal val superpixelRuler: Int? = null +) { + + fun toInternal(): Internal { + return Internal( + controlType = controlType.value, + enableControlImageComputation = enableComputation, + superpixelRegionSize = superpixelRegionSize, + superpixelRuler = superpixelRuler + ) + } + + @Serializable + internal class Internal( + val controlType: String?, + val enableControlImageComputation: Boolean?, + val superpixelRegionSize: Int?, + val superpixelRuler: Int? + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlType.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlType.kt new file mode 100644 index 00000000000..6d0bf1a98ab --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenControlType.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 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.ai.type + +/** Represents a control type for controlled Imagen generation/editing */ +public class ImagenControlType internal constructor(internal val value: String) { + public companion object { + + /** + * Use edge detection to ensure the new image follows the same outlines as the reference image. + */ + @JvmField public val CANNY: ImagenControlType = ImagenControlType("CONTROL_TYPE_CANNY") + + /** + * Use enhanced edge detection to ensure the new image follows the same outlines as the + * reference image. + */ + @JvmField public val SCRIBBLE: ImagenControlType = ImagenControlType("CONTROL_TYPE_SCRIBBLE") + + /** + * Use face mesh control to ensure that the new image has the same facial expressions as the + * reference image. + */ + @JvmField public val FACE_MESH: ImagenControlType = ImagenControlType("CONTROL_TYPE_FACE_MESH") + + /** + * Use color superpixels to ensure that the new image is similar in shape and color to the + * reference image. + */ + @JvmField + public val COLOR_SUPERPIXEL: ImagenControlType = + ImagenControlType("CONTROL_TYPE_COLOR_SUPERPIXEL") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditMode.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditMode.kt new file mode 100644 index 00000000000..6be2b44baab --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditMode.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 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.ai.type + +/** Represents the edit mode for Imagen */ +public class ImagenEditMode private constructor(internal val value: String) { + + public companion object { + /** Inserts a new element into an image */ + @JvmField + public val INPAINT_INSERTION: ImagenEditMode = ImagenEditMode("EDIT_MODE_INPAINT_INSERTION") + /** Removes an element from an image */ + @JvmField + public val INPAINT_REMOVAL: ImagenEditMode = ImagenEditMode("EDIT_MODE_INPAINT_REMOVAL") + /** Extends the borders of an image outwards */ + @JvmField public val OUTPAINT: ImagenEditMode = ImagenEditMode("EDIT_MODE_OUTPAINT") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditingConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditingConfig.kt new file mode 100644 index 00000000000..60e54261c1e --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenEditingConfig.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +/** + * Contains the editing settings which are not specific to a reference image + * @param editMode holds the editing mode if the request is for inpainting or outpainting + * @param editSteps the number of intermediate steps to include in the editing process + */ +@PublicPreviewAPI +public class ImagenEditingConfig( + internal val editMode: ImagenEditMode? = null, + internal val editSteps: Int? = null, +) { + internal fun toInternal(): Internal { + return Internal(baseSteps = editSteps) + } + + @Serializable + internal data class Internal( + val baseSteps: Int?, + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt index 6e2466da496..79ed3fa186b 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGCSImage.kt @@ -22,6 +22,5 @@ package com.google.firebase.ai.type * @param gcsUri Contains the `gs://` URI for the image. * @param mimeType Contains the MIME type of the image (for example, `"image/png"`). */ -@PublicPreviewAPI internal class ImagenGCSImage internal constructor(public val gcsUri: String, public val mimeType: String) {} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt index b59ed4d0e44..1b4407a12a6 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationConfig.kt @@ -28,7 +28,6 @@ package com.google.firebase.ai.type */ import kotlin.jvm.JvmField -@PublicPreviewAPI public class ImagenGenerationConfig( public val negativePrompt: String? = null, public val numberOfImages: Int? = 1, @@ -109,7 +108,6 @@ public class ImagenGenerationConfig( * } * ``` */ -@PublicPreviewAPI public fun imagenGenerationConfig( init: ImagenGenerationConfig.Builder.() -> Unit ): ImagenGenerationConfig { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt index 67f13cff199..f8956eafab2 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenGenerationResponse.kt @@ -27,7 +27,6 @@ import kotlinx.serialization.Serializable * @param filteredReason if fewer images were generated than were requested, this field will contain * the reason they were filtered out. */ -@PublicPreviewAPI public class ImagenGenerationResponse internal constructor(public val images: List, public val filteredReason: String?) { @@ -42,7 +41,7 @@ internal constructor(public val images: List, public val filteredReason: Stri internal fun toPublicInline() = ImagenGenerationResponse( images = predictions.filter { it.mimeType != null }.map { it.toPublicInline() }, - null, + predictions.firstNotNullOfOrNull { it.raiFilteredReason }, ) } @@ -52,10 +51,24 @@ internal constructor(public val images: List, public val filteredReason: Stri val gcsUri: String? = null, val mimeType: String? = null, val raiFilteredReason: String? = null, + val safetyAttributes: ImagenSafetyAttributes? = null, ) { internal fun toPublicInline() = ImagenInlineImage(Base64.decode(bytesBase64Encoded!!, Base64.NO_WRAP), mimeType!!) internal fun toPublicGCS() = ImagenGCSImage(gcsUri!!, mimeType!!) } + + @Serializable + internal data class ImagenSafetyAttributes( + val categories: List? = null, + val scores: List? = null + ) { + internal fun toPublic(): Map { + if (categories == null || scores == null) { + return emptyMap() + } + return categories.zip(scores).toMap() + } + } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt index 014763fd54c..f9bfc40575c 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImageFormat.kt @@ -26,7 +26,6 @@ import kotlinx.serialization.Serializable * means the image is permitted to be lower quality to reduce size. This parameter is not relevant * for every MIME type. */ -@PublicPreviewAPI public class ImagenImageFormat private constructor(public val mimeType: String, public val compressionQuality: Int?) { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImagePlacement.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImagePlacement.kt new file mode 100644 index 00000000000..db003a15344 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenImagePlacement.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2025 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.ai.type + +/** + * Represents where the placement of an image is within a new, larger image, usually in the context + * of an outpainting request. + */ +public class ImagenImagePlacement +private constructor(public val x: Int? = null, public val y: Int? = null) { + + /** + * If this placement is represented by coordinates this is a no-op, if its one of the enumerated + * types below, then the position is calculated based on its description + */ + internal fun normalizeToDimensions( + imageDimensions: Dimensions, + canvasDimensions: Dimensions, + ): ImagenImagePlacement { + if (this.x != null && this.y != null) { + return this + } + val halfCanvasHeight = canvasDimensions.height / 2 + val halfCanvasWidth = canvasDimensions.width / 2 + val halfImageHeight = imageDimensions.height / 2 + val halfImageWidth = imageDimensions.width / 2 + return when (this) { + CENTER -> + ImagenImagePlacement(halfCanvasWidth - halfImageWidth, halfCanvasHeight - halfImageHeight) + TOP_CENTER -> ImagenImagePlacement(halfCanvasWidth - halfImageWidth, 0) + BOTTOM_CENTER -> + ImagenImagePlacement( + halfCanvasWidth - halfImageWidth, + canvasDimensions.height - imageDimensions.height, + ) + LEFT_CENTER -> ImagenImagePlacement(0, halfCanvasHeight - halfImageHeight) + RIGHT_CENTER -> + ImagenImagePlacement( + canvasDimensions.width - imageDimensions.width, + halfCanvasHeight - halfImageHeight, + ) + TOP_RIGHT -> ImagenImagePlacement(canvasDimensions.width - imageDimensions.width, 0) + BOTTOM_LEFT -> ImagenImagePlacement(0, canvasDimensions.height - imageDimensions.height) + BOTTOM_RIGHT -> + ImagenImagePlacement( + canvasDimensions.width - imageDimensions.width, + canvasDimensions.height - imageDimensions.height, + ) + else -> { + throw IllegalStateException("Unknown ImagenImagePlacement instance, cannot normalize") + } + } + } + + public companion object { + /** + * Creates an [ImagenImagePlacement] that represents a placement in an image described by two + * coordinates. The coordinate system has 0,0 in the top left corner, and the x and y + * coordinates represent the location of the top left corner of the original image. + * @param x the x coordinate of the top left corner of the original image + * @param y the y coordinate of the top left corner of the original image + */ + @JvmStatic + public fun fromCoordinate(x: Int, y: Int): ImagenImagePlacement { + return ImagenImagePlacement(x, y) + } + + /** Center the image horizontally and vertically within the larger image */ + @JvmField public val CENTER: ImagenImagePlacement = ImagenImagePlacement() + + /** Center the image horizontally and aligned with the top edge of the larger image */ + @JvmField public val TOP_CENTER: ImagenImagePlacement = ImagenImagePlacement() + + /** Center the image horizontally and aligned with the bottom edge of the larger image */ + @JvmField public val BOTTOM_CENTER: ImagenImagePlacement = ImagenImagePlacement() + + /** Center the image vertically and aligned with the left edge of the larger image */ + @JvmField public val LEFT_CENTER: ImagenImagePlacement = ImagenImagePlacement() + + /** Center the image vertically and aligned with the right edge of the larger image */ + @JvmField public val RIGHT_CENTER: ImagenImagePlacement = ImagenImagePlacement() + + /** Align the image with the top left corner of the larger image */ + @JvmField public val TOP_LEFT: ImagenImagePlacement = ImagenImagePlacement(0, 0) + + /** Align the image with the top right corner of the larger image */ + @JvmField public val TOP_RIGHT: ImagenImagePlacement = ImagenImagePlacement() + + /** Align the image with the bottom left corner of the larger image */ + @JvmField public val BOTTOM_LEFT: ImagenImagePlacement = ImagenImagePlacement() + + /** Align the image with the bottom right corner of the larger image */ + @JvmField public val BOTTOM_RIGHT: ImagenImagePlacement = ImagenImagePlacement() + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt index 5fa1d0e183b..ccf084d6aa7 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenInlineImage.kt @@ -18,6 +18,9 @@ package com.google.firebase.ai.type import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.util.Base64 +import java.io.ByteArrayOutputStream +import kotlinx.serialization.Serializable /** * Represents an Imagen-generated image that is returned as inline data. @@ -26,9 +29,11 @@ import android.graphics.BitmapFactory * @property mimeType The IANA standard MIME type of the image data; either `"image/png"` or * `"image/jpeg"`; to request a different format, see [ImagenGenerationConfig.imageFormat]. */ -@PublicPreviewAPI public class ImagenInlineImage -internal constructor(public val data: ByteArray, public val mimeType: String) { +internal constructor( + public val data: ByteArray, + public val mimeType: String, +) { /** * Returns the image as an Android OS native [Bitmap] so that it can be saved or sent to the UI. @@ -36,4 +41,19 @@ internal constructor(public val data: ByteArray, public val mimeType: String) { public fun asBitmap(): Bitmap { return BitmapFactory.decodeByteArray(data, 0, data.size) } + + @Serializable internal data class Internal(val bytesBase64Encoded: String) + + internal fun toInternal(): Internal { + val base64 = Base64.encodeToString(data, Base64.NO_WRAP) + return Internal(base64) + } +} + +@PublicPreviewAPI +public fun Bitmap.toImagenInlineImage(): ImagenInlineImage { + val byteArrayOutputStream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + return ImagenInlineImage(data = byteArray, mimeType = "image/jpeg") } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskConfig.kt new file mode 100644 index 00000000000..64a9a4376e3 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +internal class ImagenMaskConfig( + internal val maskType: ImagenMaskMode, + internal val dilation: Double? = null, + internal val classes: List? = null +) { + internal fun toInternal(): Internal { + return Internal(maskType.value, dilation, classes) + } + + @Serializable + internal data class Internal( + val maskMode: String, + val dilation: Double?, + val maskClasses: List? + ) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskMode.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskMode.kt new file mode 100644 index 00000000000..b83a20381e2 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenMaskMode.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 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.ai.type + +internal class ImagenMaskMode private constructor(internal val value: String) { + companion object { + val USER_PROVIDED: ImagenMaskMode = ImagenMaskMode("MASK_MODE_USER_PROVIDED") + val BACKGROUND: ImagenMaskMode = ImagenMaskMode("MASK_MODE_BACKGROUND") + val FOREGROUND: ImagenMaskMode = ImagenMaskMode("MASK_MODE_FOREGROUND") + val SEMANTIC: ImagenMaskMode = ImagenMaskMode("MASK_MODE_SEMANTIC") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt index 5daa354bbbd..baecec0cfb4 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenPersonFilterLevel.kt @@ -17,7 +17,6 @@ package com.google.firebase.ai.type /** A filter used to prevent images from containing depictions of children or people. */ -@PublicPreviewAPI public class ImagenPersonFilterLevel private constructor(internal val internalVal: String) { public companion object { /** @@ -25,8 +24,7 @@ public class ImagenPersonFilterLevel private constructor(internal val internalVa * * > Important: Generation of images containing people or faces may require your use case to be * reviewed and approved by Cloud support; see the - * [Responsible AI and usage - * guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) + * [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) * for more details. */ @JvmField public val ALLOW_ALL: ImagenPersonFilterLevel = ImagenPersonFilterLevel("allow_all") @@ -35,8 +33,7 @@ public class ImagenPersonFilterLevel private constructor(internal val internalVa * * > Important: Generation of images containing people or faces may require your use case to be * reviewed and approved by Cloud support; see the - * [Responsible AI and usage - * guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) + * [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen) * for more details. */ @JvmField diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenReferenceImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenReferenceImage.kt new file mode 100644 index 00000000000..59eb927e687 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenReferenceImage.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2025 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.ai.type + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import com.google.firebase.ai.common.GenerateImageRequest +import kotlinx.serialization.Serializable + +/** Represents an reference image for an Imagen editing request */ +@PublicPreviewAPI +public abstract class ImagenReferenceImage +internal constructor( + internal val maskConfig: ImagenMaskConfig? = null, + internal val subjectConfig: ImagenSubjectConfig? = null, + internal val styleConfig: ImagenStyleConfig? = null, + internal val controlConfig: ImagenControlConfig? = null, + public val image: ImagenInlineImage? = null, + public val referenceId: Int? = null, +) { + + internal fun toInternal(optionalReferenceId: Int): Internal { + val referenceType = + when (this) { + is ImagenRawImage -> GenerateImageRequest.ReferenceType.RAW + is ImagenMaskReference -> GenerateImageRequest.ReferenceType.MASK + is ImagenSubjectReference -> GenerateImageRequest.ReferenceType.SUBJECT + is ImagenStyleReference -> GenerateImageRequest.ReferenceType.STYLE + is ImagenControlReference -> GenerateImageRequest.ReferenceType.CONTROL + else -> { + throw IllegalStateException( + "${this.javaClass.simpleName} is not a known subtype of ImagenReferenceImage" + ) + } + } + return Internal( + referenceType = referenceType, + referenceImage = image?.toInternal(), + referenceId = referenceId ?: optionalReferenceId, + subjectImageConfig = subjectConfig?.toInternal(), + maskImageConfig = maskConfig?.toInternal(), + styleImageConfig = styleConfig?.toInternal(), + controlConfig = controlConfig?.toInternal(), + ) + } + + @Serializable + internal data class Internal( + val referenceType: GenerateImageRequest.ReferenceType, + val referenceImage: ImagenInlineImage.Internal?, + val referenceId: Int, + val subjectImageConfig: ImagenSubjectConfig.Internal?, + val maskImageConfig: ImagenMaskConfig.Internal?, + val styleImageConfig: ImagenStyleConfig.Internal?, + val controlConfig: ImagenControlConfig.Internal? + ) +} + +/** + * Represents a reference image (provided or generated) to bound the created image via controlled + * generation. + * @param image the image provided, required if [enableComputation] is false + * @param type the type of control reference image + * @param referenceId the reference ID for this image, to be referenced in the prompt + * @param enableComputation requests that the reference image be generated serverside instead of + * provided + * @param superpixelRegionSize if type is [ImagenControlType.COLOR_SUPERPIXEL] and + * [enableComputation] is true, this will control the size of each superpixel region in pixels for + * the generated referenced image + * @param superpixelRuler if type is [ImagenControlType.COLOR_SUPERPIXEL] and [enableComputation] is + * true, this will control the superpixel smoothness factor for the generated referenced image + */ +@PublicPreviewAPI +public class ImagenControlReference( + type: ImagenControlType, + image: ImagenInlineImage? = null, + referenceId: Int? = null, + enableComputation: Boolean? = null, + superpixelRegionSize: Int? = null, + superpixelRuler: Int? = null, +) : + ImagenReferenceImage( + controlConfig = + ImagenControlConfig(type, enableComputation, superpixelRegionSize, superpixelRuler), + image = image, + referenceId = referenceId, + ) {} + +/** + * Represents a mask for Imagen editing. This image (generated or provided) should contain only + * black and white pixels, with black representing parts of the image which should not change. + */ +@PublicPreviewAPI +public abstract class ImagenMaskReference +internal constructor(maskConfig: ImagenMaskConfig, image: ImagenInlineImage? = null) : + ImagenReferenceImage(maskConfig = maskConfig, image = image) { + + public companion object { + /** + * Generates two reference images of [ImagenRawImage] and [ImagenRawMask]. These images are + * generated in this order: + * * One [ImagenRawImage] containing the original image, padded out to the new dimensions with + * black pixels, with the original image placed at the given placement + * * One [ImagenRawMask] of the same dimensions containing white everywhere except at the + * placement original image. This is the format expected by Imagen for outpainting requests. + * + * @param image the original image + * @param newDimensions the new dimensions for outpainting. These new dimensions *must* be more + * than the original image. + * @param newPosition the placement of the original image within the new outpainted image. + */ + @JvmOverloads + @JvmStatic + public fun generateMaskAndPadForOutpainting( + image: ImagenInlineImage, + newDimensions: Dimensions, + newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER, + ): List = + generateMaskAndPadForOutpainting(image, newDimensions, newPosition, 0.01) + + /** + * Generates two reference images of [ImagenRawImage] and [ImagenRawMask]. These images are + * generated in this order: + * * One [ImagenRawImage] containing the original image, padded out to the new dimensions with + * black pixels, with the original image placed at the given placement + * * One [ImagenRawMask] of the same dimensions containing white everywhere except at the + * placement original image. This is the format expected by Imagen for outpainting requests. + * + * @param image the original image + * @param newDimensions the new dimensions for outpainting. These new dimensions *must* be more + * than the original image. + * @param newPosition the placement of the original image within the new outpainted image. + * @param dilation the dilation for the outpainting mask. See: [ImagenRawMask]. + */ + @JvmStatic + public fun generateMaskAndPadForOutpainting( + image: ImagenInlineImage, + newDimensions: Dimensions, + newPosition: ImagenImagePlacement = ImagenImagePlacement.CENTER, + dilation: Double = 0.01 + ): List { + val originalBitmap = image.asBitmap() + if ( + originalBitmap.width > newDimensions.width || originalBitmap.height > newDimensions.height + ) { + throw IllegalArgumentException( + "New Dimensions must be strictly larger than original image dimensions. Original image " + + "is:${originalBitmap.width}x${originalBitmap.height}, new dimensions are " + + "${newDimensions.width}x${newDimensions.height}" + ) + } + val normalizedPosition = + newPosition.normalizeToDimensions( + Dimensions(originalBitmap.width, originalBitmap.height), + newDimensions, + ) + + if (normalizedPosition.x == null || normalizedPosition.y == null) { + throw IllegalStateException("Error normalizing position for mask and padding.") + } + + val normalizedImageRectangle = + Rect( + normalizedPosition.x, + normalizedPosition.y, + normalizedPosition.x + originalBitmap.width, + normalizedPosition.y + originalBitmap.height, + ) + + val maskBitmap = + Bitmap.createBitmap(newDimensions.width, newDimensions.height, Bitmap.Config.RGB_565) + val newImageBitmap = + Bitmap.createBitmap(newDimensions.width, newDimensions.height, Bitmap.Config.RGB_565) + + val maskCanvas = Canvas(maskBitmap) + val newImageCanvas = Canvas(newImageBitmap) + + val blackPaint = Paint().apply { color = Color.BLACK } + val whitePaint = Paint().apply { color = Color.WHITE } + + // Fill the mask with white, then draw a black rectangle where the image is. + maskCanvas.drawPaint(whitePaint) + maskCanvas.drawRect(normalizedImageRectangle, blackPaint) + + // fill the image with black, and then draw the bitmap into the corresponding spot + newImageCanvas.drawPaint(blackPaint) + newImageCanvas.drawBitmap(originalBitmap, null, normalizedImageRectangle, null) + return listOf( + ImagenRawImage(newImageBitmap.toImagenInlineImage()), + ImagenRawMask(maskBitmap.toImagenInlineImage(), dilation), + ) + } + } +} + +/** + * A generated mask image which will auto-detect and mask out the background. The background will be + * white, and the foreground black + * @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and + * make it seem more convincing. For example, `0.05` will dilate the mask 5%. + */ +@PublicPreviewAPI +public class ImagenBackgroundMask(dilation: Double? = null) : + ImagenMaskReference(maskConfig = ImagenMaskConfig(ImagenMaskMode.BACKGROUND, dilation)) {} + +/** + * A generated mask image which will auto-detect and mask out the foreground. The background will be + * black, and the foreground white + * @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and + * make it seem more convincing. For example, `0.05` will dilate the mask 5%. + */ +@PublicPreviewAPI +public class ImagenForegroundMask(dilation: Double? = null) : + ImagenMaskReference(maskConfig = ImagenMaskConfig(ImagenMaskMode.FOREGROUND, dilation)) {} + +/** + * Represents a mask for Imagen editing. This image should contain only black and white pixels, with + * black representing parts of the image which should not change. + * + * @param mask the mask image + * @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and + * make it seem more convincing. For example, `0.05` will dilate the mask 5%. + */ +@PublicPreviewAPI +public class ImagenRawMask(mask: ImagenInlineImage, dilation: Double? = null) : + ImagenMaskReference( + maskConfig = ImagenMaskConfig(ImagenMaskMode.USER_PROVIDED, dilation), + image = mask, + ) {} + +/** + * Represents a generated mask for Imagen editing which masks out certain objects using object + * detection. + * @param classes the list of segmentation IDs for objects to detect and mask out. Find a + * [list of segmentation IDs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api-edit#segment-ids) + * in the Vertex AI documentation. + * @param dilation the amount to dilate the mask. This can help smooth the borders of an edit and + * make it seem more convincing. For example, `0.05` will dilate the mask 5%. + */ +@PublicPreviewAPI +public class ImagenSemanticMask(classes: List, dilation: Double? = null) : + ImagenMaskReference(maskConfig = ImagenMaskConfig(ImagenMaskMode.SEMANTIC, dilation, classes)) {} + +/** + * Represents a base image for Imagen editing + * @param image the image + */ +@PublicPreviewAPI +public class ImagenRawImage(image: ImagenInlineImage) : ImagenReferenceImage(image = image) {} + +/** + * A reference image for style transfer + * @param image the image representing the style you want to transfer to your original images + * @param referenceId the reference ID you can use to reference this style in your prompt + * @param description the description you can use to reference this style in your prompt + */ +@PublicPreviewAPI +public class ImagenStyleReference( + image: ImagenInlineImage, + referenceId: Int? = null, + description: String? = null, +) : + ImagenReferenceImage( + image = image, + referenceId = referenceId, + styleConfig = ImagenStyleConfig(description) + ) {} + +/** + * A reference image for generating an image with a specific subject + * @param image the image of the subject + * @param referenceId the reference ID you can use to reference this subject in your prompt + * @param description the description you can use to reference this subject in your prompt + * @param subjectType the type of the subject + */ +@PublicPreviewAPI +public class ImagenSubjectReference( + image: ImagenInlineImage, + referenceId: Int? = null, + description: String? = null, + subjectType: ImagenSubjectReferenceType? = null, +) : + ImagenReferenceImage( + image = image, + referenceId = referenceId, + subjectConfig = ImagenSubjectConfig(description, subjectType), + ) {} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt index 90872d5a15d..6ac34e96fd3 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetyFilterLevel.kt @@ -17,7 +17,6 @@ package com.google.firebase.ai.type /** Used for safety filtering. */ -@PublicPreviewAPI public class ImagenSafetyFilterLevel private constructor(internal val internalVal: String) { public companion object { /** Strongest filtering level, most strict blocking. */ diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt index 7496fc6fcff..fe13234321f 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSafetySettings.kt @@ -22,7 +22,6 @@ package com.google.firebase.ai.type * @param safetyFilterLevel Used to filter unsafe content. * @param personFilterLevel Used to filter images containing people. */ -@PublicPreviewAPI public class ImagenSafetySettings( internal val safetyFilterLevel: ImagenSafetyFilterLevel, internal val personFilterLevel: ImagenPersonFilterLevel, diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenStyleConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenStyleConfig.kt new file mode 100644 index 00000000000..222f9b3f5a3 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenStyleConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +internal class ImagenStyleConfig(val description: String?) { + + fun toInternal(): Internal { + return Internal(description) + } + + @Serializable internal data class Internal(val styleDescription: String?) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectConfig.kt new file mode 100644 index 00000000000..603a580a25e --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectConfig.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.Serializable + +internal class ImagenSubjectConfig( + val description: String?, + val type: ImagenSubjectReferenceType?, +) { + + internal fun toInternal(): Internal { + return Internal(description, type?.value) + } + + @Serializable + internal data class Internal(val subjectDescription: String?, val subjectType: String?) +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectReferenceType.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectReferenceType.kt new file mode 100644 index 00000000000..dfe77f9adf5 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ImagenSubjectReferenceType.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.ai.type + +/** Represents a type for a subject reference, specifying how it should be interpreted. */ +public class ImagenSubjectReferenceType private constructor(internal val value: String) { + + public companion object { + + /** Marks the reference type as being of a person */ + @JvmField + public val PERSON: ImagenSubjectReferenceType = + ImagenSubjectReferenceType("SUBJECT_TYPE_PERSON") + + /** Marks the reference type as being of a animal */ + @JvmField + public val ANIMAL: ImagenSubjectReferenceType = + ImagenSubjectReferenceType("SUBJECT_TYPE_ANIMAL") + + /** Marks the reference type as being of a product */ + @JvmField + public val PRODUCT: ImagenSubjectReferenceType = + ImagenSubjectReferenceType("SUBJECT_TYPE_PRODUCT") + } +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt index 36e06b184e8..856eebbdde5 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveClientSetupMessage.kt @@ -32,7 +32,9 @@ internal class LiveClientSetupMessage( // needs its own config class val generationConfig: LiveGenerationConfig.Internal?, val tools: List?, - val systemInstruction: Content.Internal? + val systemInstruction: Content.Internal?, + val inputAudioTranscription: AudioTranscriptionConfig.Internal?, + val outputAudioTranscription: AudioTranscriptionConfig.Internal?, ) { @Serializable internal class Internal(val setup: LiveClientSetup) { @@ -41,10 +43,21 @@ internal class LiveClientSetupMessage( val model: String, val generationConfig: LiveGenerationConfig.Internal?, val tools: List?, - val systemInstruction: Content.Internal? + val systemInstruction: Content.Internal?, + val inputAudioTranscription: AudioTranscriptionConfig.Internal?, + val outputAudioTranscription: AudioTranscriptionConfig.Internal?, ) } fun toInternal() = - Internal(Internal.LiveClientSetup(model, generationConfig, tools, systemInstruction)) + Internal( + Internal.LiveClientSetup( + model, + generationConfig, + tools, + systemInstruction, + inputAudioTranscription, + outputAudioTranscription + ) + ) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt index 3ded9338f9b..3e014d43162 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveGenerationConfig.kt @@ -40,13 +40,6 @@ import kotlinx.serialization.Serializable * and the topP value is 0.5, then the model will select either A or B as the next token by using * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. * - * @property candidateCount The maximum number of generated response messages to return. This value - * must be between [1, 8], inclusive. If unset, this will default to 1. - * - * - Note: Only unique candidates are returned. Higher temperatures are more likely to produce - * unique candidates. Setting `temperature` to 0 will always produce exactly one candidate - * regardless of the `candidateCount`. - * * @property presencePenalty Positive penalties. * * @property frequencyPenalty Frequency penalties. @@ -60,6 +53,11 @@ import kotlinx.serialization.Serializable * * @property speechConfig Specifies the voice configuration of the audio response from the server. * + * @property inputAudioTranscription Specifies the configuration for transcribing input audio. + * + * @property outputAudioTranscription Specifies the configuration for transcribing output audio from + * the model. + * * Refer to the * [Control generated output](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/control-generated-output) * guide for more details. @@ -70,12 +68,13 @@ private constructor( internal val temperature: Float?, internal val topK: Int?, internal val topP: Float?, - internal val candidateCount: Int?, internal val maxOutputTokens: Int?, internal val presencePenalty: Float?, internal val frequencyPenalty: Float?, internal val responseModality: ResponseModality?, - internal val speechConfig: SpeechConfig? + internal val speechConfig: SpeechConfig?, + internal val inputAudioTranscription: AudioTranscriptionConfig?, + internal val outputAudioTranscription: AudioTranscriptionConfig?, ) { /** @@ -94,33 +93,33 @@ private constructor( * * @property frequencyPenalty See [LiveGenerationConfig.frequencyPenalty] * - * @property candidateCount See [LiveGenerationConfig.candidateCount]. - * * @property maxOutputTokens See [LiveGenerationConfig.maxOutputTokens]. * * @property responseModality See [LiveGenerationConfig.responseModality] * * @property speechConfig See [LiveGenerationConfig.speechConfig] + * + * @property inputAudioTranscription see [LiveGenerationConfig.inputAudioTranscription] + * + * @property outputAudioTranscription see [LiveGenerationConfig.outputAudioTranscription] */ public class Builder { @JvmField public var temperature: Float? = null @JvmField public var topK: Int? = null @JvmField public var topP: Float? = null - @JvmField public var candidateCount: Int? = null @JvmField public var maxOutputTokens: Int? = null @JvmField public var presencePenalty: Float? = null @JvmField public var frequencyPenalty: Float? = null @JvmField public var responseModality: ResponseModality? = null @JvmField public var speechConfig: SpeechConfig? = null + @JvmField public var inputAudioTranscription: AudioTranscriptionConfig? = null + @JvmField public var outputAudioTranscription: AudioTranscriptionConfig? = null public fun setTemperature(temperature: Float?): Builder = apply { this.temperature = temperature } public fun setTopK(topK: Int?): Builder = apply { this.topK = topK } public fun setTopP(topP: Float?): Builder = apply { this.topP = topP } - public fun setCandidateCount(candidateCount: Int?): Builder = apply { - this.candidateCount = candidateCount - } public fun setMaxOutputTokens(maxOutputTokens: Int?): Builder = apply { this.maxOutputTokens = maxOutputTokens } @@ -137,18 +136,27 @@ private constructor( this.speechConfig = speechConfig } + public fun setInputAudioTranscription(config: AudioTranscriptionConfig?): Builder = apply { + this.inputAudioTranscription = config + } + + public fun setOutputAudioTranscription(config: AudioTranscriptionConfig?): Builder = apply { + this.outputAudioTranscription = config + } + /** Create a new [LiveGenerationConfig] with the attached arguments. */ public fun build(): LiveGenerationConfig = LiveGenerationConfig( temperature = temperature, topK = topK, topP = topP, - candidateCount = candidateCount, maxOutputTokens = maxOutputTokens, presencePenalty = presencePenalty, frequencyPenalty = frequencyPenalty, speechConfig = speechConfig, - responseModality = responseModality + responseModality = responseModality, + inputAudioTranscription = inputAudioTranscription, + outputAudioTranscription = outputAudioTranscription, ) } @@ -157,7 +165,6 @@ private constructor( temperature = temperature, topP = topP, topK = topK, - candidateCount = candidateCount, maxOutputTokens = maxOutputTokens, frequencyPenalty = frequencyPenalty, presencePenalty = presencePenalty, @@ -172,7 +179,6 @@ private constructor( val temperature: Float?, @SerialName("top_p") val topP: Float?, @SerialName("top_k") val topK: Int?, - @SerialName("candidate_count") val candidateCount: Int?, @SerialName("max_output_tokens") val maxOutputTokens: Int?, @SerialName("presence_penalty") val presencePenalty: Float? = null, @SerialName("frequency_penalty") val frequencyPenalty: Float? = null, @@ -201,7 +207,6 @@ private constructor( * temperature = 0.75f * topP = 0.5f * topK = 30 - * candidateCount = 4 * maxOutputTokens = 300 * ... * } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt index 5ab520af474..a250f4a13c9 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveServerMessage.kt @@ -42,7 +42,9 @@ import kotlinx.serialization.json.jsonObject * play it out in realtime. */ @PublicPreviewAPI -public class LiveServerContent( +public class LiveServerContent +@Deprecated("This class should not be constructed, only received from the Server") +public constructor( /** * The content that the model has generated as part of the current conversation with the user. * @@ -82,25 +84,43 @@ public class LiveServerContent( * [interrupted] -> [turnComplete]. */ public val generationComplete: Boolean, + + /** + * The input transcription. The transcription is independent to the model turn which means it + * doesn't imply any ordering between transcription and model turn. + */ + public val inputTranscription: Transcription?, + + /** + * The output transcription. The transcription is independent to the model turn which means it + * doesn't imply any ordering between transcription and model turn. + */ + public val outputTranscription: Transcription? ) : LiveServerMessage { @OptIn(ExperimentalSerializationApi::class) @Serializable internal data class Internal( - val modelTurn: Content.Internal? = null, - val interrupted: Boolean = false, - val turnComplete: Boolean = false, - val generationComplete: Boolean = false + val modelTurn: Content.Internal?, + val interrupted: Boolean?, + val turnComplete: Boolean?, + val generationComplete: Boolean?, + val inputTranscription: Transcription.Internal?, + val outputTranscription: Transcription.Internal? ) @Serializable internal data class InternalWrapper(val serverContent: Internal) : InternalLiveServerMessage { @OptIn(ExperimentalSerializationApi::class) - override fun toPublic() = - LiveServerContent( + override fun toPublic(): LiveServerContent { + // WhenMajor(Revisit the decision to make these have default values) + return LiveServerContent( serverContent.modelTurn?.toPublic(), - serverContent.interrupted, - serverContent.turnComplete, - serverContent.generationComplete + serverContent.interrupted ?: false, + serverContent.turnComplete ?: false, + serverContent.generationComplete ?: false, + serverContent.inputTranscription?.toPublic(), + serverContent.outputTranscription?.toPublic() ) + } } } @@ -135,7 +155,8 @@ public class LiveServerToolCall(public val functionCalls: List toolCall.functionCalls.map { functionCall -> FunctionCallPart( name = functionCall.name, - args = functionCall.args.orEmpty().mapValues { it.value ?: JsonNull } + args = functionCall.args.orEmpty().mapValues { it.value ?: JsonNull }, + id = functionCall.id ) } ) @@ -183,7 +204,7 @@ internal object LiveServerMessageSerializer : LiveServerToolCallCancellation.InternalWrapper.serializer() else -> throw SerializationException( - "The given subclass of LiveServerMessage (${javaClass.simpleName}) is not supported in the serialization yet." + "Unknown LiveServerMessage response type. Keys found: ${jsonObject.keys}" ) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt index 1f84c18a53b..ea5daee30f3 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt @@ -17,35 +17,49 @@ package com.google.firebase.ai.type import android.Manifest.permission.RECORD_AUDIO +import android.annotation.SuppressLint +import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioTrack +import android.os.Process +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy import android.util.Log import androidx.annotation.RequiresPermission +import androidx.core.content.ContextCompat +import com.google.firebase.BuildConfig +import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.JSON import com.google.firebase.ai.common.util.CancelledCoroutineScope import com.google.firebase.ai.common.util.accumulateUntil import com.google.firebase.ai.common.util.childJob import com.google.firebase.annotations.concurrent.Blocking -import io.ktor.client.plugins.websocket.ClientWebSocketSession +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.websocket.Frame import io.ktor.websocket.close import io.ktor.websocket.readBytes import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.yield import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @@ -56,16 +70,27 @@ import kotlinx.serialization.json.Json @OptIn(ExperimentalSerializationApi::class) public class LiveSession internal constructor( - private val session: ClientWebSocketSession, + private val session: DefaultClientWebSocketSession, @Blocking private val blockingDispatcher: CoroutineContext, - private var audioHelper: AudioHelper? = null + private var audioHelper: AudioHelper? = null, + private val firebaseApp: FirebaseApp, ) { /** - * Coroutine scope that we batch data on for [startAudioConversation]. + * Coroutine scope that we batch data on for network related behavior. * * Makes it easy to stop all the work with [stopAudioConversation] by just cancelling the scope. */ - private var scope = CancelledCoroutineScope + private var networkScope = CancelledCoroutineScope + + /** + * Coroutine scope that we batch data on for audio recording and playback. + * + * Separate from [networkScope] to ensure interchanging of dispatchers doesn't cause any deadlocks + * or issues. + * + * Makes it easy to stop all the work with [stopAudioConversation] by just cancelling the scope. + */ + private var audioScope = CancelledCoroutineScope /** * Playback audio data sent from the model. @@ -93,8 +118,69 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { + startAudioConversation(functionCallHandler, false) + } + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. The [FunctionResponsePart] that the callback function returns will be + * automatically sent to the model. + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public suspend fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null, + enableInterruptions: Boolean = false, + ) { + startAudioConversation( + functionCallHandler = functionCallHandler, + transcriptHandler = null, + enableInterruptions = enableInterruptions + ) + } + + /** + * Starts an audio conversation with the model, which can only be stopped using + * [stopAudioConversation] or [close]. + * + * @param functionCallHandler A callback function that is invoked whenever the model receives a + * function call. The [FunctionResponsePart] that the callback function returns will be + * automatically sent to the model. + * + * @param transcriptHandler A callback function that is invoked whenever the model receives a + * transcript. The first [Transcription] object is the input transcription, and the second is the + * output transcription. + * + * @param enableInterruptions If enabled, allows the user to speak over or interrupt the model's + * ongoing reply. + * + * **WARNING**: The user interruption feature relies on device-specific support, and may not be + * consistently available. + */ + @RequiresPermission(RECORD_AUDIO) + public suspend fun startAudioConversation( + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null, + transcriptHandler: ((Transcription?, Transcription?) -> Unit)? = null, + enableInterruptions: Boolean = false, + ) { + + val context = firebaseApp.applicationContext + if ( + ContextCompat.checkSelfPermission(context, RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED + ) { + throw PermissionMissingException("Audio access not provided by the user") + } + FirebaseAIException.catchAsync { - if (scope.isActive) { + if (networkScope.isActive || audioScope.isActive) { Log.w( TAG, "startAudioConversation called after the recording has already started. " + @@ -102,13 +188,14 @@ internal constructor( ) return@catchAsync } - - scope = CoroutineScope(blockingDispatcher + childJob()) + networkScope = + CoroutineScope(blockingDispatcher + childJob() + CoroutineName("LiveSession Network")) + audioScope = CoroutineScope(audioDispatcher + childJob() + CoroutineName("LiveSession Audio")) audioHelper = AudioHelper.build() recordUserAudio() - processModelResponses(functionCallHandler) - listenForModelPlayback() + processModelResponses(functionCallHandler, transcriptHandler) + listenForModelPlayback(enableInterruptions) } } @@ -123,7 +210,8 @@ internal constructor( FirebaseAIException.catch { if (!startedReceiving.getAndSet(false)) return@catch - scope.cancel() + networkScope.cancel() + audioScope.cancel() playBackQueue.clear() audioHelper?.release() @@ -131,6 +219,12 @@ internal constructor( } } + /** Indicates whether the underlying websocket connection is active. */ + public fun isClosed(): Boolean = !(session.isActive && !session.incoming.tryReceive().isClosed) + + /** Indicates whether an audio conversation is being used for this session object. */ + public fun isAudioConversationActive(): Boolean = (audioHelper != null) + /** * Receives responses from the model for both streaming and standard requests. * @@ -152,7 +246,6 @@ internal constructor( while (true) { val response = session.incoming.tryReceive() if (response.isClosed || !startedReceiving.get()) break - response .getOrNull() ?.let { @@ -161,8 +254,9 @@ internal constructor( ) } ?.let { emit(it.toPublic()) } - - yield() + // delay uses a different scheduler in the backend, so it's "stickier" in its + // enforcement when compared to yield. + delay(0) } } .onCompletion { stopAudioConversation() } @@ -189,7 +283,8 @@ internal constructor( FirebaseAIException.catch { if (!startedReceiving.getAndSet(false)) return@catch - scope.cancel() + networkScope.cancel() + audioScope.cancel() playBackQueue.clear() audioHelper?.release() @@ -217,6 +312,59 @@ internal constructor( } } + /** + * Sends an audio input stream to the model, using the realtime API. + * + * To learn more about audio formats, and the required state they should be provided in, see the + * docs on + * [Supported audio formats](https://cloud.google.com/vertex-ai/generative-ai/docs/live-api#supported-audio-formats) + * + * @param audio Raw audio data used to update the model on the client's conversation. For best + * results, send 16-bit PCM audio at 24kHz. + */ + public suspend fun sendAudioRealtime(audio: InlineData) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString(BidiGenerateContentRealtimeInputSetup(audio = audio).toInternal()) + session.send(Frame.Text(jsonString)) + } + } + + /** + * Sends a video frame to the model, using the realtime API. + * + * Instead of raw video data, the model expects individual frames of the video, sent as images. + * + * If your video has audio, send it separately through [sendAudioRealtime]. + * + * For better performance, frames can also be sent at a lower rate than the video; even as low as + * 1 frame per second. + * + * @param video Encoded image data extracted from a frame of the video, used to update the model + * on the client's conversation, with the corresponding IANA standard MIME type of the video frame + * data (e.g., `image/png`, `image/jpeg`, etc.). + */ + public suspend fun sendVideoRealtime(video: InlineData) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString(BidiGenerateContentRealtimeInputSetup(video = video).toInternal()) + session.send(Frame.Text(jsonString)) + } + } + + /** + * Sends a text input stream to the model, using the realtime API. + * + * @param text Text content to append to the current client's conversation. + */ + public suspend fun sendTextRealtime(text: String) { + FirebaseAIException.catchAsync { + val jsonString = + Json.encodeToString(BidiGenerateContentRealtimeInputSetup(text = text).toInternal()) + session.send(Frame.Text(jsonString)) + } + } + /** * Streams client data to the model. * @@ -224,13 +372,17 @@ internal constructor( * * @param mediaChunks The list of [MediaData] instances representing the media data to be sent. */ + @Deprecated("Use sendAudioRealtime, sendVideoRealtime, or sendTextRealtime instead") public suspend fun sendMediaStream( mediaChunks: List, ) { FirebaseAIException.catchAsync { val jsonString = Json.encodeToString( - BidiGenerateContentRealtimeInputSetup(mediaChunks.map { (it.toInternal()) }).toInternal() + BidiGenerateContentRealtimeInputSetup( + mediaChunks.map { InlineData(it.data, it.mimeType) } + ) + .toInternal() ) session.send(Frame.Text(jsonString)) } @@ -285,10 +437,16 @@ internal constructor( audioHelper ?.listenToRecording() ?.buffer(UNLIMITED) + ?.flowOn(audioDispatcher) ?.accumulateUntil(MIN_BUFFER_SIZE) - ?.onEach { sendMediaStream(listOf(MediaData(it, "audio/pcm"))) } + ?.onEach { + sendAudioRealtime(InlineData(it, "audio/pcm")) + // delay uses a different scheduler in the backend, so it's "stickier" in its enforcement + // when compared to yield. + delay(0) + } ?.catch { throw FirebaseAIException.from(it) } - ?.launchIn(scope) + ?.launchIn(networkScope) } /** @@ -296,13 +454,14 @@ internal constructor( * * Audio messages are added to [playBackQueue]. * - * Launched asynchronously on [scope]. + * Launched asynchronously on [networkScope]. * * @param functionCallHandler A callback function that is invoked whenever the server receives a * function call. */ private fun processModelResponses( - functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? + functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)?, + transcriptHandler: ((Transcription?, Transcription?) -> Unit)? ) { receive() .onEach { @@ -331,6 +490,9 @@ internal constructor( ) } is LiveServerContent -> { + if (it.inputTranscription != null || it.outputTranscription != null) { + transcriptHandler?.invoke(it.inputTranscription, it.outputTranscription) + } if (it.interrupted) { playBackQueue.clear() } else { @@ -349,7 +511,7 @@ internal constructor( } } } - .launchIn(scope) + .launchIn(networkScope) } /** @@ -357,25 +519,30 @@ internal constructor( * * Polls [playBackQueue] for data, and calls [AudioHelper.playAudio] when data is received. * - * Launched asynchronously on [scope]. + * Launched asynchronously on [networkScope]. */ - private fun listenForModelPlayback() { - scope.launch { + private fun listenForModelPlayback(enableInterruptions: Boolean = false) { + audioScope.launch { while (isActive) { val playbackData = playBackQueue.poll() if (playbackData == null) { // The model playback queue is complete, so we can continue recording // TODO(b/408223520): Conditionally resume when param is added - audioHelper?.resumeRecording() - yield() + if (!enableInterruptions) { + audioHelper?.resumeRecording() + } + // delay uses a different scheduler in the backend, so it's "stickier" in its enforcement + // when compared to yield. + delay(0) } else { /** * We pause the recording while the model is speaking to avoid interrupting it because of * no echo cancellation */ // TODO(b/408223520): Conditionally pause when param is added - audioHelper?.pauseRecording() - + if (!enableInterruptions) { + audioHelper?.pauseRecording() + } audioHelper?.playAudio(playbackData) } } @@ -423,15 +590,31 @@ internal constructor( * * End of turn is derived from user activity (eg; end of speech). */ - internal class BidiGenerateContentRealtimeInputSetup(val mediaChunks: List) { + internal class BidiGenerateContentRealtimeInputSetup( + val mediaChunks: List? = null, + val audio: InlineData? = null, + val video: InlineData? = null, + val text: String? = null + ) { @Serializable internal class Internal(val realtimeInput: BidiGenerateContentRealtimeInput) { @Serializable internal data class BidiGenerateContentRealtimeInput( - val mediaChunks: List + val mediaChunks: List?, + val audio: InlineData.Internal?, + val video: InlineData.Internal?, + val text: String? ) } - fun toInternal() = Internal(Internal.BidiGenerateContentRealtimeInput(mediaChunks)) + fun toInternal() = + Internal( + Internal.BidiGenerateContentRealtimeInput( + mediaChunks?.map { it.toInternal() }, + audio?.toInternal(), + video?.toInternal(), + text + ) + ) } private companion object { @@ -442,5 +625,38 @@ internal constructor( AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT ) + @SuppressLint("ThreadPoolCreation") + val audioDispatcher = + Executors.newCachedThreadPool(AudioThreadFactory()).asCoroutineDispatcher() + } +} + +internal class AudioThreadFactory : ThreadFactory { + private val threadCount = AtomicLong() + private val policy: ThreadPolicy = audioPolicy() + + override fun newThread(task: Runnable?): Thread? { + val thread = + DEFAULT.newThread { + Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO) + StrictMode.setThreadPolicy(policy) + task?.run() + } + thread.name = "Firebase Audio Thread #${threadCount.andIncrement}" + return thread + } + + companion object { + val DEFAULT: ThreadFactory = Executors.defaultThreadFactory() + + private fun audioPolicy(): ThreadPolicy { + val builder = ThreadPolicy.Builder().detectNetwork() + + if (BuildConfig.DEBUG) { + builder.penaltyDeath() + } + + return builder.penaltyLog().build() + } } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt index 1262027989d..7647c687934 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/MediaData.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.Serializable * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). */ @PublicPreviewAPI +@Deprecated("Use InlineData instead", ReplaceWith("InlineData")) public class MediaData(public val data: ByteArray, public val mimeType: String) { @Serializable internal class Internal( diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt index bcc7e14b657..6b578b8e46e 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt @@ -18,11 +18,12 @@ package com.google.firebase.ai.type import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.util.Log +import com.google.firebase.ai.type.ImagenImageFormat.Internal import java.io.ByteArrayOutputStream import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationException import kotlinx.serialization.json.JsonContentPolymorphicSerializer import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull @@ -31,65 +32,182 @@ import kotlinx.serialization.json.jsonObject import org.json.JSONObject /** Interface representing data sent to and received from requests. */ -public interface Part {} +public interface Part { + public val isThought: Boolean +} /** Represents text or string based data sent to and received from requests. */ -public class TextPart(public val text: String) : Part { +public class TextPart +internal constructor( + public val text: String, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { + + public constructor(text: String) : this(text, false, null) + + @Serializable + internal data class Internal( + val text: String, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart +} + +/** + * Represents the code execution result from the model. + * @property outcome The result of the execution. + * @property output The stdout from the code execution, or an error message if it failed. + * @property isThought Indicates whether the response is a thought. + */ +public class CodeExecutionResultPart +internal constructor( + public val outcome: String, + public val output: String, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { + + @Deprecated("Part of the model response. Do not instantiate directly.") + public constructor(outcome: String, output: String) : this(outcome, output, false, null) + + /** Indicates if the code execution was successful */ + public fun executionSucceeded(): Boolean = (outcome.lowercase() == "outcome_ok") + + @Serializable + internal data class Internal( + @SerialName("codeExecutionResult") val codeExecutionResult: CodeExecutionResult, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart { + + @Serializable internal data class CodeExecutionResult(val outcome: String, val output: String) + } +} + +/** + * Represents the code that was executed by the model. + * @property language The programming language of the code. + * @property code The source code to be executed. + * @property isThought Indicates whether the response is a thought. + */ +public class ExecutableCodePart +internal constructor( + public val language: String, + public val code: String, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { + + @Deprecated("Part of the model response. Do not instantiate directly.") + public constructor(language: String, code: String) : this(language, code, false, null) + + @Serializable + internal data class Internal( + @SerialName("executableCode") val executableCode: ExecutableCode, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart { - @Serializable internal data class Internal(val text: String) : InternalPart + @Serializable + internal data class ExecutableCode( + @SerialName("language") val language: String, + val code: String + ) + } } /** * Represents image data sent to and received from requests. The image is converted client-side to * JPEG encoding at 80% quality before being sent to the server. - * - * @param image [Bitmap] to convert into a [Part] */ -public class ImagePart(public val image: Bitmap) : Part { +public class ImagePart +internal constructor( + public val image: Bitmap, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { + + /** @param image [Bitmap] to convert into a [Part] */ + public constructor(image: Bitmap) : this(image, false, null) internal fun toInlineDataPart() = InlineDataPart( android.util.Base64.decode(encodeBitmapToBase64Jpeg(image), BASE_64_FLAGS), - "image/jpeg" + "image/jpeg", + isThought, + thoughtSignature ) } -/** - * Represents binary data with an associated MIME type sent to and received from requests. - * - * @param inlineData the binary data as a [ByteArray] - * @param mimeType an IANA standard MIME type. For supported values, see the - * [Vertex AI documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements) - */ -public class InlineDataPart(public val inlineData: ByteArray, public val mimeType: String) : Part { +/** Represents binary data with an associated MIME type sent to and received from requests. */ +public class InlineDataPart +internal constructor( + public val inlineData: ByteArray, + public val mimeType: String, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { - @Serializable - internal data class Internal(@SerialName("inlineData") val inlineData: InlineData) : - InternalPart { + /** + * @param inlineData the binary data as a [ByteArray] + * @param mimeType an IANA standard MIME type. For supported values, see the + * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). + */ + public constructor( + inlineData: ByteArray, + mimeType: String + ) : this(inlineData, mimeType, false, null) - @Serializable - internal data class InlineData(@SerialName("mimeType") val mimeType: String, val data: Base64) - } + @Serializable + internal data class Internal( + @SerialName("inlineData") val inlineData: InlineData.Internal, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart } /** - * Represents function call name and params received from requests. - * - * @param name the name of the function to call - * @param args the function parameters and values as a [Map] - * @param id Unique id of the function call. If present, the returned [FunctionResponsePart] should - * have a matching `id` field. + * Represents binary data with an associated MIME type. + * @property data the binary data as a [ByteArray] + * @property mimeType an IANA standard MIME type. */ +public class InlineData(public val data: ByteArray, public val mimeType: String) { + @Serializable internal data class Internal(val mimeType: String, val data: Base64) + + internal fun toInternal() = + Internal(mimeType, android.util.Base64.encodeToString(data, BASE_64_FLAGS)) +} + +/** Represents function call name and params received from requests. */ public class FunctionCallPart -@JvmOverloads -constructor( +internal constructor( public val name: String, public val args: Map, - public val id: String? = null + public val id: String? = null, + public override val isThought: Boolean, + internal val thoughtSignature: String? ) : Part { + /** + * @param name the name of the function to call + * @param args the function parameters and values as a [Map] + * @param id Unique id of the function call. If present, the returned [FunctionResponsePart] + * should have a matching `id` field. + */ + @JvmOverloads + public constructor( + name: String, + args: Map, + id: String? = null, + ) : this(name, args, id, false, null) + @Serializable - internal data class Internal(val functionCall: FunctionCall) : InternalPart { + internal data class Internal( + val functionCall: FunctionCall, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart { @Serializable internal data class FunctionCall( @@ -100,23 +218,34 @@ constructor( } } -/** - * Represents function call output to be returned to the model when it requests a function call. - * - * @param name The name of the called function. - * @param response The response produced by the function as a [JSONObject]. - * @param id Matching `id` for a [FunctionCallPart], if one was provided. - */ +/** Represents function call output to be returned to the model when it requests a function call. */ public class FunctionResponsePart -@JvmOverloads -constructor( +internal constructor( public val name: String, public val response: JsonObject, - public val id: String? = null + public val id: String? = null, + public override val isThought: Boolean, + internal val thoughtSignature: String? ) : Part { + /** + * @param name The name of the called function. + * @param response The response produced by the function as a [JSONObject]. + * @param id Matching `id` for a [FunctionCallPart], if one was provided. + */ + @JvmOverloads + public constructor( + name: String, + response: JsonObject, + id: String? = null + ) : this(name, response, id, false, null) + @Serializable - internal data class Internal(val functionResponse: FunctionResponse) : InternalPart { + internal data class Internal( + val functionResponse: FunctionResponse, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart { @Serializable internal data class FunctionResponse( @@ -131,27 +260,42 @@ constructor( } } -/** - * Represents file data stored in Cloud Storage for Firebase, referenced by URI. - * - * @param uri The `"gs://"`-prefixed URI of the file in Cloud Storage for Firebase, for example, - * `"gs://bucket-name/path/image.jpg"` - * @param mimeType an IANA standard MIME type. For supported MIME type values see the - * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). - */ -public class FileDataPart(public val uri: String, public val mimeType: String) : Part { +/** Represents file data stored in Cloud Storage for Firebase, referenced by URI. */ +public class FileDataPart +internal constructor( + public val uri: String, + public val mimeType: String, + public override val isThought: Boolean, + internal val thoughtSignature: String? +) : Part { + + /** + * @param uri The `"gs://"`-prefixed URI of the file in Cloud Storage for Firebase, for example, + * `"gs://bucket-name/path/image.jpg"` + * @param mimeType an IANA standard MIME type. For supported MIME type values see the + * [Firebase documentation](https://firebase.google.com/docs/vertex-ai/input-file-requirements). + */ + public constructor(uri: String, mimeType: String) : this(uri, mimeType, false, null) @Serializable - internal data class Internal(@SerialName("file_data") val fileData: FileData) : InternalPart { + internal data class Internal( + @SerialName("file_data") val fileData: FileData, + val thought: Boolean? = null, + val thoughtSignature: String? = null + ) : InternalPart { @Serializable internal data class FileData( @SerialName("mime_type") val mimeType: String, - @SerialName("file_uri") val fileUri: String, + @SerialName("file_uri") val fileUri: String ) } } +internal data class UnknownPart(public override val isThought: Boolean = false) : Part { + @Serializable internal data class Internal(val thought: Boolean? = null) : InternalPart +} + /** Returns the part as a [String] if it represents text, and null otherwise */ public fun Part.asTextOrNull(): String? = (this as? TextPart)?.text @@ -172,41 +316,75 @@ internal const val BASE_64_FLAGS = android.util.Base64.NO_WRAP internal object PartSerializer : JsonContentPolymorphicSerializer(InternalPart::class) { + + private val TAG = PartSerializer::javaClass.name + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { val jsonObject = element.jsonObject return when { "text" in jsonObject -> TextPart.Internal.serializer() + "executableCode" in jsonObject -> ExecutableCodePart.Internal.serializer() + "codeExecutionResult" in jsonObject -> CodeExecutionResultPart.Internal.serializer() "functionCall" in jsonObject -> FunctionCallPart.Internal.serializer() "functionResponse" in jsonObject -> FunctionResponsePart.Internal.serializer() "inlineData" in jsonObject -> InlineDataPart.Internal.serializer() "fileData" in jsonObject -> FileDataPart.Internal.serializer() - else -> throw SerializationException("Unknown Part type") + else -> { + Log.w(TAG, "Unknown part type received, ignoring.") + UnknownPart.Internal.serializer() + } } } } internal fun Part.toInternal(): InternalPart { return when (this) { - is TextPart -> TextPart.Internal(text) + is TextPart -> TextPart.Internal(text, isThought, thoughtSignature) is ImagePart -> InlineDataPart.Internal( - InlineDataPart.Internal.InlineData("image/jpeg", encodeBitmapToBase64Jpeg(image)) + InlineData.Internal("image/jpeg", encodeBitmapToBase64Jpeg(image)), + isThought, + thoughtSignature ) is InlineDataPart -> InlineDataPart.Internal( - InlineDataPart.Internal.InlineData( + InlineData.Internal( mimeType, android.util.Base64.encodeToString(inlineData, BASE_64_FLAGS) - ) + ), + isThought, + thoughtSignature ) is FunctionCallPart -> - FunctionCallPart.Internal(FunctionCallPart.Internal.FunctionCall(name, args, id)) + FunctionCallPart.Internal( + FunctionCallPart.Internal.FunctionCall(name, args, id), + isThought, + thoughtSignature + ) is FunctionResponsePart -> FunctionResponsePart.Internal( - FunctionResponsePart.Internal.FunctionResponse(name, response, id) + FunctionResponsePart.Internal.FunctionResponse(name, response, id), + isThought, + thoughtSignature ) is FileDataPart -> - FileDataPart.Internal(FileDataPart.Internal.FileData(mimeType = mimeType, fileUri = uri)) + FileDataPart.Internal( + FileDataPart.Internal.FileData(mimeType = mimeType, fileUri = uri), + isThought, + thoughtSignature + ) + is ExecutableCodePart -> + ExecutableCodePart.Internal( + ExecutableCodePart.Internal.ExecutableCode(language, code), + isThought, + thoughtSignature + ) + is CodeExecutionResultPart -> + CodeExecutionResultPart.Internal( + CodeExecutionResultPart.Internal.CodeExecutionResult(outcome, output), + isThought, + thoughtSignature + ) else -> throw com.google.firebase.ai.type.SerializationException( "The given subclass of Part (${javaClass.simpleName}) is not supported in the serialization yet." @@ -223,24 +401,48 @@ private fun encodeBitmapToBase64Jpeg(input: Bitmap): String { internal fun InternalPart.toPublic(): Part { return when (this) { - is TextPart.Internal -> TextPart(text) + is TextPart.Internal -> TextPart(text, thought ?: false, thoughtSignature) is InlineDataPart.Internal -> { val data = android.util.Base64.decode(inlineData.data, BASE_64_FLAGS) if (inlineData.mimeType.contains("image")) { - ImagePart(decodeBitmapFromImage(data)) + ImagePart(decodeBitmapFromImage(data), thought ?: false, thoughtSignature) } else { - InlineDataPart(data, inlineData.mimeType) + InlineDataPart(data, inlineData.mimeType, thought ?: false, thoughtSignature) } } is FunctionCallPart.Internal -> FunctionCallPart( functionCall.name, functionCall.args.orEmpty().mapValues { it.value ?: JsonNull }, - functionCall.id + functionCall.id, + thought ?: false, + thoughtSignature ) is FunctionResponsePart.Internal -> - FunctionResponsePart(functionResponse.name, functionResponse.response, functionResponse.id) - is FileDataPart.Internal -> FileDataPart(fileData.mimeType, fileData.fileUri) + FunctionResponsePart( + functionResponse.name, + functionResponse.response, + functionResponse.id, + thought ?: false, + thoughtSignature + ) + is FileDataPart.Internal -> + FileDataPart(fileData.mimeType, fileData.fileUri, thought ?: false, thoughtSignature) + is ExecutableCodePart.Internal -> + ExecutableCodePart( + executableCode.language, + executableCode.code, + thought ?: false, + thoughtSignature + ) + is CodeExecutionResultPart.Internal -> + CodeExecutionResultPart( + codeExecutionResult.outcome, + codeExecutionResult.output, + thought ?: false, + thoughtSignature + ) + is UnknownPart.Internal -> UnknownPart() else -> throw com.google.firebase.ai.type.SerializationException( "Unsupported part type \"${javaClass.simpleName}\" provided. This model may not be supported by this SDK." diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt index bc4a53cc8eb..3979fbc0a20 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/PublicPreviewAPI.kt @@ -23,4 +23,5 @@ package com.google.firebase.ai.type "This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.", ) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) public annotation class PublicPreviewAPI() diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt index 9eaa4590aad..1dfa4ddecb0 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Schema.kt @@ -42,6 +42,12 @@ internal constructor( public val properties: Map? = null, public val required: List? = null, public val items: Schema? = null, + public val title: String? = null, + public val minItems: Int? = null, + public val maxItems: Int? = null, + public val minimum: Double? = null, + public val maximum: Double? = null, + public val anyOf: List? = null, ) { public companion object { @@ -53,12 +59,12 @@ internal constructor( */ @JvmStatic @JvmOverloads - public fun boolean(description: String? = null, nullable: Boolean = false): Schema = - Schema( - description = description, - nullable = nullable, - type = "BOOLEAN", - ) + public fun boolean( + description: String? = null, + nullable: Boolean = false, + title: String? = null, + ): Schema = + Schema(description = description, nullable = nullable, type = "BOOLEAN", title = title) /** * Returns a [Schema] for a 32-bit signed integer number. @@ -73,12 +79,21 @@ internal constructor( @JvmStatic @JvmName("numInt") @JvmOverloads - public fun integer(description: String? = null, nullable: Boolean = false): Schema = + public fun integer( + description: String? = null, + nullable: Boolean = false, + title: String? = null, + minimum: Double? = null, + maximum: Double? = null, + ): Schema = Schema( description = description, format = "int32", nullable = nullable, type = "INTEGER", + title = title, + minimum = minimum, + maximum = maximum, ) /** @@ -90,11 +105,20 @@ internal constructor( @JvmStatic @JvmName("numLong") @JvmOverloads - public fun long(description: String? = null, nullable: Boolean = false): Schema = + public fun long( + description: String? = null, + nullable: Boolean = false, + title: String? = null, + minimum: Double? = null, + maximum: Double? = null, + ): Schema = Schema( description = description, nullable = nullable, type = "INTEGER", + title = title, + minimum = minimum, + maximum = maximum, ) /** @@ -106,8 +130,21 @@ internal constructor( @JvmStatic @JvmName("numDouble") @JvmOverloads - public fun double(description: String? = null, nullable: Boolean = false): Schema = - Schema(description = description, nullable = nullable, type = "NUMBER") + public fun double( + description: String? = null, + nullable: Boolean = false, + title: String? = null, + minimum: Double? = null, + maximum: Double? = null, + ): Schema = + Schema( + description = description, + nullable = nullable, + type = "NUMBER", + title = title, + minimum = minimum, + maximum = maximum, + ) /** * Returns a [Schema] for a single-precision floating-point number. @@ -123,8 +160,22 @@ internal constructor( @JvmStatic @JvmName("numFloat") @JvmOverloads - public fun float(description: String? = null, nullable: Boolean = false): Schema = - Schema(description = description, nullable = nullable, type = "NUMBER", format = "float") + public fun float( + description: String? = null, + nullable: Boolean = false, + title: String? = null, + minimum: Double? = null, + maximum: Double? = null, + ): Schema = + Schema( + description = description, + nullable = nullable, + type = "NUMBER", + format = "float", + title = title, + minimum = minimum, + maximum = maximum, + ) /** * Returns a [Schema] for a string. @@ -139,13 +190,15 @@ internal constructor( public fun string( description: String? = null, nullable: Boolean = false, - format: StringFormat? = null + format: StringFormat? = null, + title: String? = null, ): Schema = Schema( description = description, format = format?.value, nullable = nullable, - type = "STRING" + type = "STRING", + title = title, ) /** @@ -155,6 +208,7 @@ internal constructor( * `String` and values of type [Schema]. * * **Example:** A `city` could be represented with the following object `Schema`. + * * ``` * Schema.obj(mapOf( * "name" to Schema.string(), @@ -176,6 +230,7 @@ internal constructor( optionalProperties: List = emptyList(), description: String? = null, nullable: Boolean = false, + title: String? = null, ): Schema { if (!properties.keys.containsAll(optionalProperties)) { throw IllegalArgumentException( @@ -188,6 +243,7 @@ internal constructor( properties = properties, required = properties.keys.minus(optionalProperties.toSet()).toList(), type = "OBJECT", + title = title, ) } @@ -203,20 +259,25 @@ internal constructor( public fun array( items: Schema, description: String? = null, - nullable: Boolean = false + nullable: Boolean = false, + title: String? = null, + minItems: Int? = null, + maxItems: Int? = null, ): Schema = Schema( description = description, nullable = nullable, items = items, type = "ARRAY", + title = title, + minItems = minItems, + maxItems = maxItems, ) /** * Returns a [Schema] for an enumeration. * * For example, the cardinal directions can be represented as: - * * ``` * Schema.enumeration(listOf("north", "east", "south", "west"), "Cardinal directions") * ``` @@ -230,7 +291,8 @@ internal constructor( public fun enumeration( values: List, description: String? = null, - nullable: Boolean = false + nullable: Boolean = false, + title: String? = null, ): Schema = Schema( description = description, @@ -238,29 +300,169 @@ internal constructor( nullable = nullable, enum = values, type = "STRING", + title = title, ) + + /** + * Returns a [Schema] representing a value that must conform to *any* (one of) the provided + * sub-schema. + * + * Example: A field that can hold either a simple userID or a more detailed user object. + * + * ``` + * Schema.anyOf( listOf( Schema.integer(description = "User ID"), Schema.obj( mapOf( + * "userID" to Schema.integer(description = "User ID"), + * "username" to Schema.string(description = "Username") + * ))) + * ``` + * + * @param schemas The list of valid schemas which could be here + */ + @JvmStatic + public fun anyOf(schemas: List): Schema = Schema(type = "ANYOF", anyOf = schemas) } - internal fun toInternal(): Internal = - Internal( - type, + internal fun toInternalOpenApi(): InternalOpenAPI { + val cleanedType = + if (type == "ANYOF") { + null + } else { + type + } + return InternalOpenAPI( + cleanedType, description, format, nullable, enum, - properties?.mapValues { it.value.toInternal() }, + properties?.mapValues { it.value.toInternalOpenApi() }, required, - items?.toInternal(), + items?.toInternalOpenApi(), + title, + minItems, + maxItems, + minimum, + maximum, + anyOf?.map { it.toInternalOpenApi() }, ) + } + + internal fun toInternalJson(): InternalJson { + val outType = + if (type == "ANYOF" || (type == "STRING" && format == "enum")) { + null + } else { + type.lowercase() + } + + val (outMinimum, outMaximum) = + if (outType == "integer" && format == "int32") { + (minimum ?: Integer.MIN_VALUE.toDouble()) to (maximum ?: Integer.MAX_VALUE.toDouble()) + } else { + minimum to maximum + } + + val outFormat = + if ( + (outType == "integer" && format == "int32") || + (outType == "number" && format == "float") || + format == "enum" + ) { + null + } else { + format + } + + if (nullable == true) { + return InternalJsonNullable( + outType?.let { listOf(it, "null") }, + description, + outFormat, + enum?.let { + buildList { + addAll(it) + add("null") + } + }, + properties?.mapValues { it.value.toInternalJson() }, + required, + items?.toInternalJson(), + title, + minItems, + maxItems, + outMinimum, + outMaximum, + anyOf?.map { it.toInternalJson() }, + ) + } + return InternalJsonNonNull( + outType, + description, + outFormat, + enum, + properties?.mapValues { it.value.toInternalJson() }, + required, + items?.toInternalJson(), + title, + minItems, + maxItems, + outMinimum, + outMaximum, + anyOf?.map { it.toInternalJson() }, + ) + } + @Serializable - internal data class Internal( - val type: String, + internal data class InternalOpenAPI( + val type: String? = null, val description: String? = null, val format: String? = null, val nullable: Boolean? = false, val enum: List? = null, - val properties: Map? = null, + val properties: Map? = null, val required: List? = null, - val items: Internal? = null, + val items: InternalOpenAPI? = null, + val title: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val minimum: Double? = null, + val maximum: Double? = null, + val anyOf: List? = null, ) + + @Serializable internal sealed interface InternalJson + + @Serializable + internal data class InternalJsonNonNull( + val type: String? = null, + val description: String? = null, + val format: String? = null, + val enum: List? = null, + val properties: Map? = null, + val required: List? = null, + val items: InternalJson? = null, + val title: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val minimum: Double? = null, + val maximum: Double? = null, + val anyOf: List? = null, + ) : InternalJson + + @Serializable + internal data class InternalJsonNullable( + val type: List? = null, + val description: String? = null, + val format: String? = null, + val enum: List? = null, + val properties: Map? = null, + val required: List? = null, + val items: InternalJson? = null, + val title: String? = null, + val minItems: Int? = null, + val maxItems: Int? = null, + val minimum: Double? = null, + val maximum: Double? = null, + val anyOf: List? = null, + ) : InternalJson } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt new file mode 100644 index 00000000000..b220de57e86 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2025 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.ai.type + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** Configuration parameters for thinking features. */ +public class ThinkingConfig +private constructor( + internal val thinkingBudget: Int? = null, + internal val includeThoughts: Boolean? = null +) { + + public class Builder() { + @JvmField + @set:JvmSynthetic // hide void setter from Java + public var thinkingBudget: Int? = null + + @JvmField + @set:JvmSynthetic // hide void setter from Java + public var includeThoughts: Boolean? = null + + /** + * Indicates the thinking budget in tokens. `0` is disabled. `-1` is dynamic. The default values + * and allowed ranges are model dependent. + */ + public fun setThinkingBudget(thinkingBudget: Int): Builder = apply { + this.thinkingBudget = thinkingBudget + } + + /** + * Indicates whether to request the model to include the thoughts parts in the response. + * + * Keep in mind that once enabled, you should check for the `isThought` property when processing + * a `Part` instance to correctly handle both thoughts and the actual response. + * + * The default value is `false`. + */ + public fun setIncludeThoughts(includeThoughts: Boolean): Builder = apply { + this.includeThoughts = includeThoughts + } + + public fun build(): ThinkingConfig = + ThinkingConfig(thinkingBudget = thinkingBudget, includeThoughts = includeThoughts) + } + + internal fun toInternal() = Internal(thinkingBudget, includeThoughts) + + @Serializable + internal data class Internal( + @SerialName("thinking_budget") val thinkingBudget: Int? = null, + val includeThoughts: Boolean? = null + ) +} + +/** + * Helper method to construct a [ThinkingConfig] in a DSL-like manner. + * + * Example Usage: + * ``` + * thinkingConfig { + * thinkingBudget = 0 // disable thinking + * } + * ``` + */ +public fun thinkingConfig(init: ThinkingConfig.Builder.() -> Unit): ThinkingConfig { + val builder = ThinkingConfig.Builder() + builder.init() + return builder.build() +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt index 83391166bd4..43a66a10d62 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Tool.kt @@ -20,22 +20,41 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject /** - * Contains a set of function declarations that the model has access to. These can be used to gather - * information, or complete tasks - * - * @param functionDeclarations The set of functions that this tool allows the model access to + * Contains a set of tools (like function declarations) that the model has access to. These tools + * can be used to gather information or complete tasks. */ public class Tool -internal constructor(internal val functionDeclarations: List?) { - internal fun toInternal() = Internal(functionDeclarations?.map { it.toInternal() } ?: emptyList()) +@OptIn(PublicPreviewAPI::class) +internal constructor( + internal val functionDeclarations: List?, + internal val googleSearch: GoogleSearch?, + internal val codeExecution: JsonObject?, + @property:PublicPreviewAPI internal val urlContext: UrlContext?, +) { + + @OptIn(PublicPreviewAPI::class) + internal fun toInternal() = + Internal( + functionDeclarations?.map { it.toInternal() } ?: emptyList(), + googleSearch = this.googleSearch?.toInternal(), + codeExecution = this.codeExecution, + urlContext = this.urlContext?.toInternal() + ) + + @OptIn(PublicPreviewAPI::class) @Serializable internal data class Internal( val functionDeclarations: List? = null, + val googleSearch: GoogleSearch.Internal? = null, // This is a json object because it is not possible to make a data class with no parameters. val codeExecution: JsonObject? = null, + val urlContext: UrlContext.Internal? = null, ) public companion object { + @OptIn(PublicPreviewAPI::class) + private val codeExecutionInstance by lazy { Tool(null, null, JsonObject(emptyMap()), null) } + /** * Creates a [Tool] instance that provides the model with access to the [functionDeclarations]. * @@ -43,7 +62,48 @@ internal constructor(internal val functionDeclarations: List): Tool { - return Tool(functionDeclarations) + @OptIn(PublicPreviewAPI::class) return Tool(functionDeclarations, null, null, null) + } + + /** Creates a [Tool] instance that allows the model to use code execution. */ + @JvmStatic + public fun codeExecution(): Tool { + return codeExecutionInstance + } + + /** + * Creates a [Tool] instance that allows you to provide additional context to the models in the + * form of public web URLs. By including URLs in your request, the Gemini model will access the + * content from those pages to inform and enhance its response. + * + * @param urlContext Specifies the URL context configuration. + * @return A [Tool] configured for URL context. + */ + @PublicPreviewAPI + @JvmStatic + public fun urlContext(urlContext: UrlContext = UrlContext()): Tool { + return Tool(null, null, null, urlContext) + } + + /** + * Creates a [Tool] instance that allows the model to use grounding with Google Search. + * + * Grounding with Google Search can be used to allow the model to connect to Google Search to + * access and incorporate up-to-date information from the web into it's responses. + * + * When using this feature, you are required to comply with the "grounding with Google Search" + * usage requirements for your chosen API provider: + * [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) + * or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) + * section within the Service Specific Terms). + * + * @param googleSearch An empty [GoogleSearch] object. The presence of this object in the list + * of tools enables the model to use Google Search. + * @return A [Tool] configured for Google Search. + */ + @JvmStatic + public fun googleSearch(googleSearch: GoogleSearch = GoogleSearch()): Tool { + @OptIn(PublicPreviewAPI::class) return Tool(null, googleSearch, null, null) } } } diff --git a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Transcription.kt similarity index 60% rename from firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Transcription.kt index 380bfa3c30b..6dc65e5abdb 100644 --- a/firebase-vertexai/src/main/kotlin/com/google/firebase/vertexai/type/ImagenGCSImage.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Transcription.kt @@ -14,14 +14,20 @@ * limitations under the License. */ -package com.google.firebase.vertexai.type +package com.google.firebase.ai.type + +import kotlinx.serialization.Serializable /** - * Represents an Imagen-generated image that is contained in Google Cloud Storage. - * - * @param gcsUri Contains the `gs://` URI for the image. - * @param mimeType Contains the MIME type of the image (for example, `"image/png"`). + * Audio transcription message. + * @property text Transcription text */ -@PublicPreviewAPI -internal class ImagenGCSImage -internal constructor(public val gcsUri: String, public val mimeType: String) {} +public class Transcription internal constructor(public val text: String?) { + + @Serializable + internal data class Internal(val text: String?) { + fun toPublic(): Transcription { + return Transcription(text) + } + } +} diff --git a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UrlContext.kt similarity index 66% rename from firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UrlContext.kt index 88c5890f9bf..a2f15e25043 100644 --- a/firebase-perf/ktx/src/test/kotlin/com/google/firebase/perf/metrics/TestUtil.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UrlContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.google.firebase.perf.metrics +package com.google.firebase.ai.type -fun getTraceCounter(trace: Trace): Map { - return trace.getCounters() -} +import kotlinx.serialization.Serializable + +/** Specifies the URL context configuration. */ +@PublicPreviewAPI +public class UrlContext { + @Serializable internal class Internal() -fun getTraceCounterCount(trace: Trace, counterName: String): Long { - return trace.getCounters().get(counterName)!!.getCount() + internal fun toInternal() = Internal() } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt index 1b858a1e6cd..60e0ef72e72 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/UsageMetadata.kt @@ -28,15 +28,42 @@ import kotlinx.serialization.Serializable * prompt. * @param candidatesTokensDetails The breakdown, by modality, of how many tokens are consumed by the * candidates. + * @param toolUsePromptTokensDetails The breakdown, by modality, of how many tokens are consumed by + * tools. + * @param thoughtsTokenCount The number of tokens used by the model's internal "thinking" process. + * @param toolUsePromptTokenCount The number of tokens used by tools. */ -public class UsageMetadata( +public class UsageMetadata +internal constructor( public val promptTokenCount: Int, public val candidatesTokenCount: Int?, public val totalTokenCount: Int, public val promptTokensDetails: List, public val candidatesTokensDetails: List, + public val thoughtsTokenCount: Int, + public val toolUsePromptTokenCount: Int, + public val toolUsePromptTokensDetails: List ) { + @Deprecated("Not intended for public use") + public constructor( + promptTokenCount: Int, + candidatesTokenCount: Int?, + totalTokenCount: Int, + promptTokensDetails: List, + candidatesTokensDetails: List, + thoughtsTokenCount: Int + ) : this( + promptTokenCount, + candidatesTokenCount, + totalTokenCount, + promptTokensDetails, + candidatesTokensDetails, + thoughtsTokenCount, + 0, + emptyList() + ) + @Serializable internal data class Internal( val promptTokenCount: Int? = null, @@ -44,6 +71,9 @@ public class UsageMetadata( val totalTokenCount: Int? = null, val promptTokensDetails: List? = null, val candidatesTokensDetails: List? = null, + val thoughtsTokenCount: Int? = null, + val toolUsePromptTokenCount: Int? = null, + val toolUsePromptTokensDetails: List? = null, ) { internal fun toPublic(): UsageMetadata = @@ -52,7 +82,11 @@ public class UsageMetadata( candidatesTokenCount ?: 0, totalTokenCount ?: 0, promptTokensDetails = promptTokensDetails?.map { it.toPublic() } ?: emptyList(), - candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } ?: emptyList() + candidatesTokensDetails = candidatesTokensDetails?.map { it.toPublic() } ?: emptyList(), + thoughtsTokenCount ?: 0, + toolUsePromptTokenCount ?: 0, + toolUsePromptTokensDetails = toolUsePromptTokensDetails?.map { it.toPublic() } + ?: emptyList(), ) } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt index 7053fc986cf..2a27dc681dc 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voice.kt @@ -20,8 +20,8 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * Various voices supported by the server. The list of all voices can be found - * [here](https://cloud.google.com/text-to-speech/docs/chirp3-hd) + * Various voices supported by the server. In the documentation, find the list of + * [all supported voices](https://cloud.google.com/text-to-speech/docs/chirp3-hd). */ @PublicPreviewAPI public class Voice public constructor(public val voiceName: String) { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt index d5e1f738dc2..5f97b3b36fb 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Voices.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** Various voices supported by the server */ -@Deprecated("Please use the Voice class instead.", ReplaceWith("Voice")) +@Deprecated("Use the Voice class instead.", ReplaceWith("Voice")) @PublicPreviewAPI public class Voices private constructor(public val ordinal: Int) { diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt index 967254a096c..2aac8f7a0d2 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt @@ -23,6 +23,7 @@ import com.google.firebase.ai.type.ResponseStoppedException import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.util.goldenDevAPIStreamingFile import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.shouldBe import io.ktor.http.HttpStatusCode import kotlin.time.Duration.Companion.seconds @@ -30,7 +31,10 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withTimeout import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) internal class DevAPIStreamingSnapshotTests { private val testTimeout = 5.seconds @@ -64,6 +68,23 @@ internal class DevAPIStreamingSnapshotTests { } } + @Test + fun `reply with a single empty part`() = + goldenDevAPIStreamingFile("streaming-success-empty-parts.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + // Second to last response has no parts + responseList[5].candidates.first().content.parts.shouldBeEmpty() + responseList.last().candidates.first().apply { + finishReason shouldBe FinishReason.STOP + content.parts.isEmpty() shouldBe false + } + } + } + @Test fun `prompt blocked for safety`() = goldenDevAPIStreamingFile("streaming-failure-prompt-blocked-safety.txt") { diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt index 91a263f8c66..e0ba386091b 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt @@ -18,13 +18,20 @@ package com.google.firebase.ai import com.google.firebase.ai.type.FinishReason import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.ResponseStoppedException import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.type.UrlRetrievalStatus import com.google.firebase.ai.util.goldenDevAPIUnaryFile import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import io.ktor.http.HttpStatusCode import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout @@ -39,9 +46,24 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().content.parts.shouldNotBeEmpty() + } + } + + @Test + fun `only prompt feedback reply`() = + goldenDevAPIUnaryFile("unary-failure-only-prompt-feedback.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldBeEmpty() + + // Check response from accessors + response.text.shouldBeNull() + response.functionCalls.shouldBeEmpty() + response.inlineDataParts.shouldBeEmpty() } } @@ -51,9 +73,9 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().finishReason shouldBe FinishReason.STOP - response.candidates.first().content.parts.isEmpty() shouldBe false + response.candidates.first().content.parts.shouldNotBeEmpty() } } @@ -63,11 +85,11 @@ internal class DevAPIUnarySnapshotTests { withTimeout(testTimeout) { val response = model.generateContent("prompt") - response.candidates.isEmpty() shouldBe false + response.candidates.shouldNotBeEmpty() response.candidates.first().citationMetadata?.citations?.size shouldBe 4 response.candidates.first().citationMetadata?.citations?.forEach { - it.startIndex shouldNotBe null - it.endIndex shouldNotBe null + it.startIndex.shouldNotBeNull() + it.endIndex.shouldNotBeNull() } } } @@ -95,4 +117,111 @@ internal class DevAPIUnarySnapshotTests { goldenDevAPIUnaryFile("unary-failure-unknown-model.json", HttpStatusCode.NotFound) { withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } } + + // This test case can be removed once b/422779395 is + // fixed. + @Test + fun `google search grounding empty grounding chunks`() = + goldenDevAPIUnaryFile("unary-success-google-search-grounding-empty-grounding-chunks.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + groundingMetadata.groundingChunks.forEach { it.web.shouldBeNull() } + } + } + + @OptIn(PublicPreviewAPI::class) + @Test + fun `url context`() = + goldenDevAPIUnaryFile("unary-success-url-context.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + + val urlContextMetadata = candidate.urlContextMetadata + urlContextMetadata.shouldNotBeNull() + + urlContextMetadata.urlMetadata.shouldNotBeEmpty() + urlContextMetadata.urlMetadata.shouldHaveSize(1) + urlContextMetadata.urlMetadata[0].retrievedUrl.shouldBe("https://berkshirehathaway.com") + urlContextMetadata.urlMetadata[0].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.SUCCESS) + + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + groundingMetadata.groundingChunks.forEach { it.web.shouldNotBeNull() } + groundingMetadata.groundingSupports.shouldHaveSize(4) + + val usageMetadata = response.usageMetadata + + usageMetadata.shouldNotBeNull() + usageMetadata.toolUsePromptTokenCount.shouldBeGreaterThan(0) + usageMetadata.toolUsePromptTokensDetails.shouldHaveSize(1) + } + } + + @OptIn(PublicPreviewAPI::class) + @Test + fun `url context mixed validity`() = + goldenDevAPIUnaryFile("unary-success-url-context-mixed-validity.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + + val urlContextMetadata = candidate.urlContextMetadata + urlContextMetadata.shouldNotBeNull() + + urlContextMetadata.urlMetadata.shouldNotBeEmpty() + urlContextMetadata.urlMetadata.shouldHaveSize(3) + urlContextMetadata.urlMetadata[0] + .retrievedUrl + .shouldBe("https://a-completely-non-existent-url-for-testing.org") + urlContextMetadata.urlMetadata[0].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.ERROR) + urlContextMetadata.urlMetadata[1].retrievedUrl.shouldBe("https://ai.google.dev") + urlContextMetadata.urlMetadata[1].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.SUCCESS) + + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + groundingMetadata.groundingChunks.forEach { it.web.shouldNotBeNull() } + groundingMetadata.groundingSupports.shouldHaveSize(3) + + val usageMetadata = response.usageMetadata + + usageMetadata.shouldNotBeNull() + usageMetadata.toolUsePromptTokenCount.shouldBeGreaterThan(0) + usageMetadata.toolUsePromptTokensDetails.shouldHaveSize(1) + } + } + + @Test + fun `thinking function call and thought signature`() = + goldenDevAPIUnaryFile("unary-success-thinking-function-call-thought-summary-signature.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isNotEmpty() + response.thoughtSummary.shouldNotBeNull() + response.thoughtSummary?.isNotEmpty() + response.functionCalls.isNotEmpty() + response.functionCalls.first().let { + it.thoughtSignature.shouldNotBeNull() + it.thoughtSignature.isNotEmpty() + } + // There's no text in the response + response.text.shouldBeNull() + } + } } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt index 8301f48d968..e12e133efc8 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt @@ -23,6 +23,7 @@ import com.google.firebase.ai.common.util.doBlocking import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.type.TextPart @@ -41,7 +42,6 @@ import io.ktor.http.content.TextContent import io.ktor.http.headersOf import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import org.junit.Before import org.junit.Test @@ -72,7 +72,7 @@ internal class GenerativeModelTesting { val apiController = APIController( "super_cool_test_key", - "gemini-1.5-flash", + "gemini-2.5-flash", RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), mockEngine, TEST_CLIENT_ID, @@ -84,7 +84,7 @@ internal class GenerativeModelTesting { val generativeModel = GenerativeModel( - "gemini-1.5-flash", + "gemini-2.5-flash", systemInstruction = content { text("system instruction") }, controller = apiController ) @@ -120,7 +120,7 @@ internal class GenerativeModelTesting { val apiController = APIController( "super_cool_test_key", - "gemini-1.5-flash", + "gemini-2.5-flash", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -133,7 +133,7 @@ internal class GenerativeModelTesting { // Creating the val generativeModel = GenerativeModel( - "projects/PROJECTID/locations/INVALID_LOCATION/publishers/google/models/gemini-1.5-flash", + "projects/PROJECTID/locations/INVALID_LOCATION/publishers/google/models/gemini-2.5-flash", controller = apiController ) @@ -146,7 +146,7 @@ internal class GenerativeModelTesting { exception.message shouldContain "location" } - @OptIn(ExperimentalSerializationApi::class) + @OptIn(PublicPreviewAPI::class) private fun generateContentResponseAsJsonString(text: String): String { return JSON.encodeToString( GenerateContentResponse.Internal( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt index f9bdf8c835f..b275807d9b5 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/SchemaTests.kt @@ -19,7 +19,9 @@ package com.google.firebase.ai import com.google.firebase.ai.type.Schema import com.google.firebase.ai.type.StringFormat import io.kotest.assertions.json.shouldEqualJson +import java.io.File import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.ClassDiscriminatorMode import kotlinx.serialization.json.Json import org.junit.Test @@ -93,7 +95,7 @@ internal class SchemaTests { """ .trimIndent() - Json.encodeToString(schemaDeclaration.toInternal()).shouldEqualJson(expectedJson) + Json.encodeToString(schemaDeclaration.toInternalOpenApi()).shouldEqualJson(expectedJson) } @Test @@ -216,6 +218,70 @@ internal class SchemaTests { """ .trimIndent() - Json.encodeToString(schemaDeclaration.toInternal()).shouldEqualJson(expectedJson) + Json.encodeToString(schemaDeclaration.toInternalOpenApi()).shouldEqualJson(expectedJson) } + + @Test + fun `schema encoding openAPI spec test`() { + val expectedSerialization = getSchemaJson("open-api-schema.json") + val serializedSchema = JSON_ENCODER.encodeToString(TEST_SCHEMA.toInternalOpenApi()) + serializedSchema.shouldEqualJson(expectedSerialization) + } + + @Test + fun `schema encoding jsonSchema spec test`() { + val expectedSerialization = getSchemaJson("json-schema.json") + val serializedSchema = JSON_ENCODER.encodeToString(TEST_SCHEMA.toInternalJson()) + serializedSchema.shouldEqualJson(expectedSerialization) + } + + internal fun getSchemaJson(filename: String): String { + return File("src/test/resources/vertexai-sdk-test-data/mock-responses/schema/${filename}") + .readText() + } + + private val JSON_ENCODER = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE } + + private val TEST_SCHEMA = + Schema.obj( + properties = + mapOf( + "integerTest" to Schema.integer(title = "integerTest", nullable = true), + "longTest" to + Schema.long( + title = "longTest", + nullable = false, + minimum = 0.0, + maximum = 5.0, + description = "a test long" + ), + "floatTest" to Schema.float(title = "floatTest", nullable = false), + "doubleTest" to Schema.double(title = "doubleTest", nullable = true), + "listTest" to + Schema.array( + items = Schema.integer(nullable = false), + title = "listTest", + nullable = false, + minItems = 0, + maxItems = 5 + ), + "booleanTest" to Schema.boolean(title = "booleanTest", nullable = false), + "stringTest" to + Schema.string(title = "stringTest", format = StringFormat.Custom("email")), + "objTest" to + Schema.obj( + properties = + mapOf( + "testInt" to Schema.integer(title = "testInt", nullable = false), + ), + title = "objTest", + description = "class kdoc should be used if property kdocs aren't present", + nullable = false + ), + "enumTest" to Schema.enumeration(values = listOf("val1", "val2", "val3")) + ), + optionalProperties = listOf("booleanTest"), + description = "A test kdoc", + nullable = false + ) } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt index d00f75c2714..476d68261d2 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/SerializationTests.kt @@ -20,11 +20,26 @@ import com.google.firebase.ai.common.util.descriptorToJson import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.GoogleSearch +import com.google.firebase.ai.type.GroundingAttribution +import com.google.firebase.ai.type.GroundingChunk +import com.google.firebase.ai.type.GroundingMetadata +import com.google.firebase.ai.type.GroundingSupport +import com.google.firebase.ai.type.ImagenReferenceImage import com.google.firebase.ai.type.ModalityTokenCount +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.Schema +import com.google.firebase.ai.type.SearchEntryPoint +import com.google.firebase.ai.type.Segment +import com.google.firebase.ai.type.Tool +import com.google.firebase.ai.type.UrlContext +import com.google.firebase.ai.type.UrlContextMetadata +import com.google.firebase.ai.type.UrlMetadata +import com.google.firebase.ai.type.WebGroundingChunk import io.kotest.assertions.json.shouldEqualJson import org.junit.Test +@OptIn(PublicPreviewAPI::class) internal class SerializationTests { @Test fun `test countTokensResponse serialization as Json`() { @@ -150,7 +165,10 @@ internal class SerializationTests { }, "groundingMetadata": { "${'$'}ref": "GroundingMetadata" - } + }, + "urlContextMetadata": { + "${'$'}ref": "UrlContextMetadata" + } } } """ @@ -159,6 +177,195 @@ internal class SerializationTests { expectedJsonAsString shouldEqualJson actualJson.toString() } + @Test + fun `test GroundingMetadata serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GroundingMetadata", + "type": "object", + "properties": { + "webSearchQueries": { "type": "array", "items": { "type": "string" } }, + "searchEntryPoint": { "${'$'}ref": "SearchEntryPoint" }, + "retrievalQueries": { "type": "array", "items": { "type": "string" } }, + "groundingAttribution": { "type": "array", "items": { "${'$'}ref": "GroundingAttribution" } }, + "groundingChunks": { "type": "array", "items": { "${'$'}ref": "GroundingChunk" } }, + "groundingSupports": { "type": "array", "items": { "${'$'}ref": "GroundingSupport" } } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GroundingMetadata.Internal.serializer().descriptor) + println(actualJson) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test SearchEntryPoint serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "SearchEntryPoint", + "type": "object", + "properties": { + "renderedContent": { "type": "string" }, + "sdkBlob": { "type": "string" } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(SearchEntryPoint.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GroundingChunk serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GroundingChunk", + "type": "object", + "properties": { + "web": { "${'$'}ref": "WebGroundingChunk" } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GroundingChunk.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test WebGroundingChunk serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "WebGroundingChunk", + "type": "object", + "properties": { + "uri": { "type": "string" }, + "title": { "type": "string" }, + "domain": { "type": "string" } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(WebGroundingChunk.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GroundingSupport serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GroundingSupport", + "type": "object", + "properties": { + "segment": { + "${'$'}ref": "Segment" + }, + "groundingChunkIndices": { + "type": "array", + "items": { "type": "integer" } + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GroundingSupport.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Segment serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "Segment", + "type": "object", + "properties": { + "startIndex": { "type": "integer" }, + "endIndex": { "type": "integer" }, + "partIndex": { "type": "integer" }, + "text": { "type": "string" } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Segment.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test UrlContextMetadata serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "UrlContextMetadata", + "type": "object", + "properties": { + "urlMetadata": { "type": "array", "items": { "${'$'}ref": "UrlMetadata" } } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(UrlContextMetadata.Internal.serializer().descriptor) + println(actualJson) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test UrlMetadata serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "UrlMetadata", + "type": "object", + "properties": { + "retrievedUrl": { + "type": "string" + }, + "urlRetrievalStatus": { + "type": "string", + "enum": [ + "UNSPECIFIED", + "SUCCESS", + "ERROR", + "PAYWALL", + "UNSAFE" + ] + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(UrlMetadata.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GroundingAttribution serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GroundingAttribution", + "type": "object", + "properties": { + "segment": { + "${'$'}ref": "Segment" + }, + "confidenceScore": { + "type": "number" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(GroundingAttribution.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + @Test fun `test Schema serialization as Json`() { /** @@ -175,18 +382,15 @@ internal class SerializationTests { "type": { "type": "string" }, - "format": { + "description": { "type": "string" }, - "description": { + "format": { "type": "string" }, "nullable": { "type": "boolean" }, - "items": { - "${'$'}ref": "Schema" - }, "enum": { "type": "array", "items": { @@ -204,12 +408,137 @@ internal class SerializationTests { "items": { "type": "string" } - } + }, + "items": { + "${'$'}ref": "Schema" + }, + "title": { + "type": "string" + }, + "minItems": { + "type": "integer" + }, + "maxItems": { + "type": "integer" + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "anyOf": { + "type": "array", + "items": { + "${'$'}ref": "Schema" + } + } } } """ .trimIndent() - val actualJson = descriptorToJson(Schema.Internal.serializer().descriptor) + val actualJson = descriptorToJson(Schema.InternalOpenAPI.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test ReferenceImage serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "ImagenReferenceImage", + "type": "object", + "properties": { + "referenceType": { + "type": "string" + }, + "referenceImage": { + "${'$'}ref": "ImagenInlineImage" + }, + "referenceId": { + "type": "integer" + }, + "subjectImageConfig": { + "${'$'}ref": "ImagenSubjectConfig" + }, + "maskImageConfig": { + "${'$'}ref": "ImagenMaskConfig" + }, + "styleImageConfig": { + "${'$'}ref": "ImagenStyleConfig" + }, + "controlConfig": { + "${'$'}ref": "ImagenControlConfig" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(ImagenReferenceImage.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test Tool serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "Tool", + "type": "object", + "properties": { + "functionDeclarations": { + "type": "array", + "items": { + "${'$'}ref": "FunctionDeclaration" + } + }, + "googleSearch": { + "${'$'}ref": "GoogleSearch" + }, + "codeExecution": { + "type": "object", + "additionalProperties": { + "${'$'}ref": "JsonElement" + } + }, + "urlContext": { + "${'$'}ref": "UrlContext" + } + } + } + """ + .trimIndent() + val actualJson = descriptorToJson(Tool.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test GoogleSearch serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "GoogleSearch", + "type": "object", + "properties": {} + } + """ + .trimIndent() + val actualJson = descriptorToJson(GoogleSearch.Internal.serializer().descriptor) + expectedJsonAsString shouldEqualJson actualJson.toString() + } + + @Test + fun `test UrlContext serialization as Json`() { + val expectedJsonAsString = + """ + { + "id": "UrlContext", + "type": "object", + "properties": {} + } + """ + .trimIndent() + val actualJson = descriptorToJson(UrlContext.Internal.serializer().descriptor) expectedJsonAsString shouldEqualJson actualJson.toString() } } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt index e6331401fde..d54cda6c7aa 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIStreamingSnapshotTests.kt @@ -27,6 +27,8 @@ import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.type.TextPart import com.google.firebase.ai.util.goldenVertexStreamingFile import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain @@ -155,7 +157,13 @@ internal class VertexAIStreamingSnapshotTests { goldenVertexStreamingFile("streaming-failure-empty-content.txt") { val responses = model.generateContentStream("prompt") - withTimeout(testTimeout) { shouldThrow { responses.collect() } } + withTimeout(testTimeout) { + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.shouldHaveSize(1) + responseList.first().candidates.first().content.parts.shouldBeEmpty() + } + } } @Test @@ -241,6 +249,10 @@ internal class VertexAIStreamingSnapshotTests { goldenVertexStreamingFile("streaming-failure-malformed-content.txt") { val responses = model.generateContentStream("prompt") - withTimeout(testTimeout) { shouldThrow { responses.collect() } } + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.shouldHaveSize(1) + responseList.first().candidates.first().content.parts.shouldBeEmpty() + } } } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt index ca1d279d288..83e4bc2b2bc 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/VertexAIUnarySnapshotTests.kt @@ -34,11 +34,16 @@ import com.google.firebase.ai.type.ServerException import com.google.firebase.ai.type.ServiceDisabledException import com.google.firebase.ai.type.TextPart import com.google.firebase.ai.type.UnsupportedUserLocationException +import com.google.firebase.ai.type.UrlRetrievalStatus import com.google.firebase.ai.util.goldenVertexUnaryFile import com.google.firebase.ai.util.shouldNotBeNullOrEmpty import io.kotest.assertions.throwables.shouldThrow import io.kotest.inspectors.forAtLeastOne +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should import io.kotest.matchers.shouldBe @@ -89,6 +94,19 @@ internal class VertexAIUnarySnapshotTests { } } + @Test + fun `response including an empty part is handled gracefully`() = + goldenVertexUnaryFile("unary-success-empty-part.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.text.shouldNotBeEmpty() + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + } + } + @Test fun `response with detailed token-based usageMetadata`() = goldenVertexUnaryFile("unary-success-basic-response-long-usage-metadata.json") { @@ -245,7 +263,9 @@ internal class VertexAIUnarySnapshotTests { fun `empty content`() = goldenVertexUnaryFile("unary-failure-empty-content.json") { withTimeout(testTimeout) { - shouldThrow { model.generateContent("prompt") } + val response = model.generateContent("prompt") + response.candidates.shouldNotBeEmpty() + response.candidates.first().content.parts.shouldBeEmpty() } } @@ -388,10 +408,12 @@ internal class VertexAIUnarySnapshotTests { } @Test - fun `malformed content`() = + fun `response including an unknown part is handled gracefully`() = goldenVertexUnaryFile("unary-failure-malformed-content.json") { withTimeout(testTimeout) { - shouldThrow { model.generateContent("prompt") } + val response = model.generateContent("prompt") + response.candidates.shouldNotBeEmpty() + response.candidates.first().content.parts.shouldBeEmpty() } } @@ -589,4 +611,150 @@ internal class VertexAIUnarySnapshotTests { shouldThrow { imagenModel.generateImages("prompt") } } } + + @Test + fun `generateImages should contain safety data`() = + goldenVertexUnaryFile("unary-success-generate-images-safety_info.json") { + withTimeout(testTimeout) { + val response = imagenModel.generateImages("prompt") + // There is no public API, but if it parses then success + } + } + + @Test + fun `google search grounding metadata is parsed correctly`() = + goldenVertexUnaryFile("unary-success-google-search-grounding.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + candidate.finishReason shouldBe FinishReason.STOP + + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.webSearchQueries.first() shouldBe "current weather in London" + groundingMetadata.searchEntryPoint.shouldNotBeNull() + groundingMetadata.searchEntryPoint?.renderedContent.shouldNotBeEmpty() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + val groundingChunk = groundingMetadata.groundingChunks.first() + groundingChunk.web.shouldNotBeNull() + groundingChunk.web?.uri.shouldNotBeEmpty() + groundingChunk.web?.title shouldBe "accuweather.com" + groundingChunk.web?.domain.shouldBeNull() + + groundingMetadata.groundingSupports.shouldNotBeEmpty() + groundingMetadata.groundingSupports.size shouldBe 3 + val groundingSupport = groundingMetadata.groundingSupports.first() + groundingSupport.segment.shouldNotBeNull() + groundingSupport.segment.startIndex shouldBe 0 + groundingSupport.segment.partIndex shouldBe 0 + groundingSupport.segment.endIndex shouldBe 56 + groundingSupport.segment.text shouldBe + "The current weather in London, United Kingdom is cloudy." + groundingSupport.groundingChunkIndices.first() shouldBe 0 + + val secondGroundingSupport = groundingMetadata.groundingSupports[1] + secondGroundingSupport.segment.shouldNotBeNull() + secondGroundingSupport.segment.startIndex shouldBe 57 + secondGroundingSupport.segment.partIndex shouldBe 0 + secondGroundingSupport.segment.endIndex shouldBe 123 + secondGroundingSupport.segment.text shouldBe + "The temperature is 67°F (19°C), but it feels like 75°F (24°C)." + secondGroundingSupport.groundingChunkIndices.first() shouldBe 1 + } + } + + @Test + fun `url context`() = + goldenVertexUnaryFile("unary-success-url-context.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + + val urlContextMetadata = candidate.urlContextMetadata + urlContextMetadata.shouldNotBeNull() + + urlContextMetadata.urlMetadata.shouldNotBeEmpty() + urlContextMetadata.urlMetadata.shouldHaveSize(1) + urlContextMetadata.urlMetadata[0].retrievedUrl.shouldBe("https://berkshirehathaway.com") + urlContextMetadata.urlMetadata[0].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.SUCCESS) + + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + groundingMetadata.groundingChunks.forEach { it.web.shouldNotBeNull() } + groundingMetadata.groundingSupports.shouldHaveSize(2) + + val usageMetadata = response.usageMetadata + + usageMetadata.shouldNotBeNull() + usageMetadata.toolUsePromptTokenCount.shouldBeGreaterThan(0) + usageMetadata.toolUsePromptTokensDetails + .shouldBeEmpty() // This isn't yet supported in Vertex AI + } + } + + @Test + fun `url context mixed validity`() = + goldenVertexUnaryFile("unary-success-url-context-mixed-validity.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + + val urlContextMetadata = candidate.urlContextMetadata + urlContextMetadata.shouldNotBeNull() + + urlContextMetadata.urlMetadata.shouldNotBeEmpty() + urlContextMetadata.urlMetadata.shouldHaveSize(3) + urlContextMetadata.urlMetadata[2] + .retrievedUrl + .shouldBe("https://a-completely-non-existent-url-for-testing.org") + urlContextMetadata.urlMetadata[2].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.ERROR) + urlContextMetadata.urlMetadata[1].retrievedUrl.shouldBe("https://ai.google.dev") + urlContextMetadata.urlMetadata[1].urlRetrievalStatus.shouldBe(UrlRetrievalStatus.SUCCESS) + + val groundingMetadata = candidate.groundingMetadata + groundingMetadata.shouldNotBeNull() + + groundingMetadata.groundingChunks.shouldNotBeEmpty() + groundingMetadata.groundingChunks.forEach { it.web.shouldNotBeNull() } + groundingMetadata.groundingSupports.shouldHaveSize(6) + + val usageMetadata = response.usageMetadata + + usageMetadata.shouldNotBeNull() + usageMetadata.toolUsePromptTokenCount.shouldBeGreaterThan(0) + usageMetadata.toolUsePromptTokensDetails + .shouldBeEmpty() // This isn't yet supported in Vertex AI + } + } + + // This test only applies to Vertex AI, since this is a bug in the backend. + @Test + fun `url context missing retrievedUrl`() = + goldenVertexUnaryFile("unary-success-url-context-missing-retrievedurl.json") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.shouldNotBeEmpty() + val candidate = response.candidates.first() + + val urlContextMetadata = candidate.urlContextMetadata + urlContextMetadata.shouldNotBeNull() + + urlContextMetadata.urlMetadata.shouldNotBeEmpty() + urlContextMetadata.urlMetadata.shouldHaveSize(20) + // Not all the retrievedUrls are null. Only the last 10. We only need to check one. + urlContextMetadata.urlMetadata.last().retrievedUrl.shouldBeNull() + urlContextMetadata.urlMetadata.last().urlRetrievalStatus.shouldNotBeNull() + } + } } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt index b1ae69c25b2..f3e818085f7 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt @@ -25,10 +25,14 @@ import com.google.firebase.ai.common.util.prepareStreamingResponse import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.FunctionCallingConfig +import com.google.firebase.ai.type.GoogleSearch +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.ai.type.RequestTimeoutException import com.google.firebase.ai.type.TextPart import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.ai.type.UrlContext import io.kotest.assertions.json.shouldContainJsonKey import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -40,7 +44,6 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf import io.ktor.utils.io.ByteChannel -import io.ktor.utils.io.close import io.ktor.utils.io.writeFully import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -112,7 +115,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, "genai-android/${BuildConfig.VERSION_NAME}", @@ -142,7 +145,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(timeout = 5.seconds, endpoint = "https://my.custom.endpoint"), mockEngine, TEST_CLIENT_ID, @@ -172,7 +175,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -199,7 +202,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -227,7 +230,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -262,6 +265,84 @@ internal class RequestFormatTests { "tool_config.function_calling_config.allowed_function_names" } + @Test + fun `google search tool serialization contains correct keys`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-2.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) + controller + .generateContentStream( + GenerateContentRequest( + model = "unused", + contents = listOf(Content.Internal(parts = listOf(TextPart.Internal("Arbitrary")))), + tools = listOf(Tool.Internal(googleSearch = GoogleSearch.Internal())), + ) + ) + .collect { channel.close() } + } + + val requestBodyAsText = (mockEngine.requestHistory.first().body as TextContent).text + + requestBodyAsText shouldContainJsonKey "tools[0].googleSearch" + } + + @Test + fun `url context tool serialization contains correct keys`() = doBlocking { + val channel = ByteChannel(autoFlush = true) + val mockEngine = MockEngine { + respond(channel, HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, "application/json")) + } + prepareStreamingResponse(createResponses("Random")).forEach { channel.writeFully(it) } + + val controller = + APIController( + "super_cool_test_key", + "gemini-pro-2.5", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) + controller + .generateContentStream( + GenerateContentRequest( + model = "unused", + contents = listOf(Content.Internal(parts = listOf(TextPart.Internal("Arbitrary")))), + tools = listOf(Tool.Internal(urlContext = UrlContext.Internal())), + ) + ) + .collect { channel.close() } + } + + val requestBodyAsText = (mockEngine.requestHistory.first().body as TextContent).text + + requestBodyAsText shouldContainJsonKey "tools[0].urlContext" + } + @Test fun `headers from HeaderProvider are added to the request`() = doBlocking { val response = JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)) @@ -281,7 +362,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -318,7 +399,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -344,7 +425,7 @@ internal class RequestFormatTests { val controller = APIController( "super_cool_test_key", - "gemini-pro-1.5", + "gemini-pro-2.5", RequestOptions(), mockEngine, TEST_CLIENT_ID, @@ -355,6 +436,7 @@ internal class RequestFormatTests { ) withTimeout(5.seconds) { + @OptIn(PublicPreviewAPI::class) controller .generateContentStream( GenerateContentRequest( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt index 6cc501cedd5..d9081cae371 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt @@ -24,6 +24,7 @@ import com.google.firebase.ai.common.JSON import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerateContentResponse +import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.TextPart import io.ktor.client.engine.mock.MockEngine @@ -32,7 +33,6 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf import io.ktor.utils.io.ByteChannel -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString import org.mockito.Mockito @@ -44,7 +44,7 @@ internal fun prepareStreamingResponse( response: List ): List = response.map { "data: ${JSON.encodeToString(it)}$SSE_SEPARATOR".toByteArray() } -@OptIn(ExperimentalSerializationApi::class) +@OptIn(PublicPreviewAPI::class) internal fun createResponses(vararg text: String): List { val candidates = text.map { Candidate.Internal(Content.Internal(parts = listOf(TextPart.Internal(it)))) } diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/ThinkingConfigTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/ThinkingConfigTest.kt new file mode 100644 index 00000000000..815699806ed --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/ThinkingConfigTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 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.ai.type + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.equals.shouldBeEqual +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Test + +internal class ThinkingConfigTest { + + @Test + fun `Basic ThinkingConfig`() { + val thinkingConfig = ThinkingConfig.Builder().setThinkingBudget(1024).build() + + val expectedJson = + """ + { + "thinking_budget": 1024 + } + """ + .trimIndent() + + Json.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson) + } + + @Test + fun `Include thought thinkingConfig`() { + val thinkingConfig = ThinkingConfig.Builder().setIncludeThoughts(true).build() + // CamelCase or snake_case work equally fine + val expectedJson = + """ + { + "includeThoughts": true + } + """ + .trimIndent() + + Json.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson) + } + + @Test + fun `thinkingConfig DSL correctly delegates to ThinkingConfig#Builder`() { + val thinkingConfig = ThinkingConfig.Builder().setThinkingBudget(1024).build() + + val thinkingConfigDsl = thinkingConfig { thinkingBudget = 1024 } + + thinkingConfig.thinkingBudget?.shouldBeEqual(thinkingConfigDsl.thinkingBudget as Int) + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt b/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt new file mode 100644 index 00000000000..41c764b624a --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/type/ToolTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2025 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.ai.type + +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.junit.Test + +internal class ToolTest { + @Test + fun `googleSearch() creates a tool with a googleSearch property`() { + val tool = Tool.googleSearch() + + tool.googleSearch.shouldNotBeNull() + tool.functionDeclarations.shouldBeNull() + tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() + } + + @Test + fun `functionDeclarations() creates a tool with functionDeclarations`() { + val functionDeclaration = FunctionDeclaration("test", "test", emptyMap()) + val tool = Tool.functionDeclarations(listOf(functionDeclaration)) + + tool.functionDeclarations?.first() shouldBe functionDeclaration + tool.googleSearch.shouldBeNull() + tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() + } + + @Test + fun `codeExecution() creates a tool with code execution`() { + val tool = Tool.codeExecution() + tool.codeExecution.shouldNotBeNull() + tool.functionDeclarations.shouldBeNull() + tool.googleSearch.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldBeNull() + } + + @OptIn(PublicPreviewAPI::class) + @Test + fun `urlContext() creates a tool with a urlContext property`() { + val tool = Tool.urlContext() + + tool.googleSearch.shouldBeNull() + tool.functionDeclarations.shouldBeNull() + tool.codeExecution.shouldBeNull() + @OptIn(PublicPreviewAPI::class) tool.urlContext.shouldNotBeNull() + } +} diff --git a/firebase-ai/src/test/resources/README.md b/firebase-ai/src/test/resources/README.md index 372846e739d..682548dd6ba 100644 --- a/firebase-ai/src/test/resources/README.md +++ b/firebase-ai/src/test/resources/README.md @@ -1,2 +1,2 @@ -Mock response files should be cloned into this directory to run unit tests. See -the Firebase AI [README](../../..#running-tests) for instructions. \ No newline at end of file +Mock response files should be cloned into this directory to run unit tests. See the Firebase AI +[README](../../..#running-tests) for instructions. diff --git a/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java index 559c4ac8a04..ef18dd94ae7 100644 --- a/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java +++ b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java @@ -21,9 +21,11 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.firebase.ai.FirebaseAI; import com.google.firebase.ai.GenerativeModel; +import com.google.firebase.ai.ImagenModel; import com.google.firebase.ai.LiveGenerativeModel; import com.google.firebase.ai.java.ChatFutures; import com.google.firebase.ai.java.GenerativeModelFutures; +import com.google.firebase.ai.java.ImagenModelFutures; import com.google.firebase.ai.java.LiveModelFutures; import com.google.firebase.ai.java.LiveSessionFutures; import com.google.firebase.ai.type.BlockReason; @@ -33,6 +35,7 @@ import com.google.firebase.ai.type.Content; import com.google.firebase.ai.type.ContentModality; import com.google.firebase.ai.type.CountTokensResponse; +import com.google.firebase.ai.type.Dimensions; import com.google.firebase.ai.type.FileDataPart; import com.google.firebase.ai.type.FinishReason; import com.google.firebase.ai.type.FunctionCallPart; @@ -43,6 +46,12 @@ import com.google.firebase.ai.type.HarmProbability; import com.google.firebase.ai.type.HarmSeverity; import com.google.firebase.ai.type.ImagePart; +import com.google.firebase.ai.type.ImagenBackgroundMask; +import com.google.firebase.ai.type.ImagenEditMode; +import com.google.firebase.ai.type.ImagenEditingConfig; +import com.google.firebase.ai.type.ImagenInlineImage; +import com.google.firebase.ai.type.ImagenMaskReference; +import com.google.firebase.ai.type.InlineData; import com.google.firebase.ai.type.InlineDataPart; import com.google.firebase.ai.type.LiveGenerationConfig; import com.google.firebase.ai.type.LiveServerContent; @@ -65,6 +74,7 @@ import com.google.firebase.concurrent.FirebaseExecutors; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; @@ -132,7 +142,6 @@ private LiveGenerationConfig getLiveConfig() { .setTopK(10) .setTopP(11.0F) .setTemperature(32.0F) - .setCandidateCount(1) .setMaxOutputTokens(0xCAFEBABE) .setFrequencyPenalty(1.0F) .setPresencePenalty(2.0F) @@ -141,6 +150,17 @@ private LiveGenerationConfig getLiveConfig() { .build(); } + private void testImagen() { + ImagenModel modelSuspend = FirebaseAI.getInstance().imagenModel(""); + ImagenModelFutures model = ImagenModelFutures.from(modelSuspend); + model.editImage( + Collections.singletonList(new ImagenBackgroundMask()), + "", + new ImagenEditingConfig(ImagenEditMode.OUTPAINT, 25)); + ImagenMaskReference.generateMaskAndPadForOutpainting( + new ImagenInlineImage(new byte[0], ""), new Dimensions(0, 0)); + } + private void testFutures(GenerativeModelFutures futures) throws Exception { Content content = new Content.Builder() @@ -346,6 +366,9 @@ public void onComplete() { byte[] bytes = new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}; session.sendMediaStream(List.of(new MediaData(bytes, "image/jxl"))); + session.sendAudioRealtime(new InlineData(bytes, "audio/jxl")); + session.sendVideoRealtime(new InlineData(bytes, "image/jxl")); + session.sendTextRealtime("text"); FunctionResponsePart functionResponse = new FunctionResponsePart("myFunction", new JsonObject(Map.of())); diff --git a/firebase-ai/update_responses.sh b/firebase-ai/update_responses.sh index 7d6ea18e0ee..881d63e2c53 100755 --- a/firebase-ai/update_responses.sh +++ b/firebase-ai/update_responses.sh @@ -17,7 +17,7 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v13.*' # The major version of mock responses to use +RESPONSES_VERSION='v15.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" diff --git a/firebase-annotations/CHANGELOG.md b/firebase-annotations/CHANGELOG.md index 5b97d49e713..b716a3fd418 100644 --- a/firebase-annotations/CHANGELOG.md +++ b/firebase-annotations/CHANGELOG.md @@ -1,3 +1,3 @@ # Unreleased -* [changed] Hid Executors from public API, as they are intended to be internal - anyhow. + +- [changed] Hid Executors from public API, as they are intended to be internal anyhow. diff --git a/firebase-annotations/firebase-annotations.gradle.kts b/firebase-annotations/firebase-annotations.gradle.kts index df2fa9b290f..fd78203e0f5 100644 --- a/firebase-annotations/firebase-annotations.gradle.kts +++ b/firebase-annotations/firebase-annotations.gradle.kts @@ -24,6 +24,6 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -tasks.withType { options.compilerArgs.add("-Werror") } +tasks.withType { options.compilerArgs.addAll(listOf("-Werror", "-Xlint:-options")) } dependencies { implementation(libs.javax.inject) } diff --git a/firebase-annotations/gradle.properties b/firebase-annotations/gradle.properties index 15b213d9867..775999190b9 100644 --- a/firebase-annotations/gradle.properties +++ b/firebase-annotations/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=17.0.0 -latestReleasedVersion=16.2.0 +version=17.0.1 +latestReleasedVersion=17.0.0 diff --git a/firebase-appdistribution-api/CHANGELOG.md b/firebase-appdistribution-api/CHANGELOG.md index 44afcf8054e..6465260ea8d 100644 --- a/firebase-appdistribution-api/CHANGELOG.md +++ b/firebase-appdistribution-api/CHANGELOG.md @@ -1,160 +1,157 @@ # Unreleased +# 16.0.0-beta17 + +- [changed] Bumped internal dependencies. + +# 16.0.0-beta16 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. +- [removed] **Breaking Change**: Stopped releasing the deprecated Kotlin extensions (KTX) module and + removed it from the Firebase Android BoM. Instead, use the KTX APIs from the main module. For + details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration). # 16.0.0-beta15 -* [unchanged] Updated to accommodate the release of the updated - [appdistro] library. +- [unchanged] Updated to accommodate the release of the updated [appdistro] library. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta14 -* [unchanged] Updated to accommodate the release of the updated - [appdistro] library. +- [unchanged] Updated to accommodate the release of the updated [appdistro] library. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta13 -* [changed] Bump internal dependencies +- [changed] Bump internal dependencies ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta12 -* [unchanged] Updated to accommodate the release of the updated - [appdistro] library. +- [unchanged] Updated to accommodate the release of the updated [appdistro] library. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta11 -* [changed] Added Kotlin extensions (KTX) APIs from - `com.google.firebase:firebase-appdistribution-api-ktx` - to `com.google.firebase:firebase-appdistribution-api` under the - `com.google.firebase.appdistribution` package. - For details, see the + +- [changed] Added Kotlin extensions (KTX) APIs from + `com.google.firebase:firebase-appdistribution-api-ktx` to + `com.google.firebase:firebase-appdistribution-api` under the `com.google.firebase.appdistribution` + package. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) -* [deprecated] All the APIs from `com.google.firebase:firebase-appdistribution-api-ktx` have been - added to - `com.google.firebase:firebase-appdistribution-api` under the - `com.google.firebase.appdistribution` package, - and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-appdistribution-api-ktx` - are now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, - see the +- [deprecated] All the APIs from `com.google.firebase:firebase-appdistribution-api-ktx` have been + added to `com.google.firebase:firebase-appdistribution-api` under the + `com.google.firebase.appdistribution` package, and all the Kotlin extensions (KTX) APIs in + `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April 2024, + we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta09 -* [feature] Improved development mode to allow all API calls to be made without having to sign in. +- [feature] Improved development mode to allow all API calls to be made without having to sign in. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no -additional updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta08 -* [fixed] Fixed an issue where a crash happened whenever a feedback - notification was shown on devices running Android 4.4 and lower. +- [fixed] Fixed an issue where a crash happened whenever a feedback notification was shown on + devices running Android 4.4 and lower. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no -additional updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta07 -* [feature] Added support for testers to attach JPEG screenshots to their - feedback. +- [feature] Added support for testers to attach JPEG screenshots to their feedback. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no -additional updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta06 -* [feature] Added support for in-app tester feedback. To learn more, see - [Collect feedback from testers](/docs/app-distribution/collect-feedback-from-testers?platform=android). -* [fixed] Fixed a bug where only the last listener added to an `UpdateTask` - using `addOnProgressListener()` would receive updates. +- [feature] Added support for in-app tester feedback. To learn more, see + [Collect feedback from testers](/docs/app-distribution/collect-feedback-from-testers?platform=android). +- [fixed] Fixed a bug where only the last listener added to an `UpdateTask` using + `addOnProgressListener()` would receive updates. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta05 -* [unchanged] Updated to accommodate the release of the updated - [appdistro] Kotlin extensions library. +- [unchanged] Updated to accommodate the release of the updated [appdistro] Kotlin extensions + library. ## Kotlin -The Kotlin extensions library transitively includes the updated - `firebase-appdistribution-api` library. The Kotlin extensions library has - the following additional updates: - -* [feature] Firebase now supports Kotlin coroutines. - With this release, we added - [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: .external} - to `firebase-appdistribution-api-ktx` as a transitive dependency, which - exposes the `Task.await()` suspend function to convert a - [`Task`](https://developers.google.com/android/guides/tasks) - into a Kotlin coroutine. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has the following additional updates: + +- [feature] Firebase now supports Kotlin coroutines. With this release, we added + [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: + .external} to `firebase-appdistribution-api-ktx` as a transitive dependency, which exposes the + `Task.await()` suspend function to convert a + [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin coroutine. # 16.0.0-beta04 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-appdistribution-api` library. The Kotlin extensions library has no -additional updates. + +The Kotlin extensions library transitively includes the updated `firebase-appdistribution-api` +library. The Kotlin extensions library has no additional updates. # 16.0.0-beta03 -* [feature] The [appdistro] SDK has been split into two libraries: - * `firebase-appdistribution-api` - The API-only library
- This new API-only library is functional only when the full - [appdistro] SDK implementation (`firebase-appdistribution`) is present. - `firebase-appdistribution-api` can be included in all +- [feature] The [appdistro] SDK has been split into two libraries: + + - `firebase-appdistribution-api` - The API-only library
This new API-only library is + functional only when the full [appdistro] SDK implementation (`firebase-appdistribution`) is + present. `firebase-appdistribution-api` can be included in all [build variants](https://developer.android.com/studio/build/build-variants){: .external}. - * `firebase-appdistribution` - The full SDK implementation
- This full SDK implementation is optional and should only be included in - pre-release builds. + - `firebase-appdistribution` - The full SDK implementation
This full SDK implementation is + optional and should only be included in pre-release builds. Visit the documentation to learn how to - [add these SDKs](/docs/app-distribution/set-up-alerts?platform=android#add-appdistro) - to your Android app. - + [add these SDKs](/docs/app-distribution/set-up-alerts?platform=android#add-appdistro) to your + Android app. ## Kotlin -With the removal of the Kotlin extensions library -`firebase-appdistribution-ktx`, its functionality has been moved to the new -API-only library: `firebase-appdistribution-api-ktx`. -This new Kotlin extensions library transitively includes the -`firebase-appdistribution-api` library. The Kotlin extensions library has no -additional updates. +With the removal of the Kotlin extensions library `firebase-appdistribution-ktx`, its functionality +has been moved to the new API-only library: `firebase-appdistribution-api-ktx`. +This new Kotlin extensions library transitively includes the `firebase-appdistribution-api` library. +The Kotlin extensions library has no additional updates. diff --git a/firebase-appdistribution-api/api.txt b/firebase-appdistribution-api/api.txt index 4e823a730f8..94c06f1068f 100644 --- a/firebase-appdistribution-api/api.txt +++ b/firebase-appdistribution-api/api.txt @@ -99,18 +99,3 @@ package com.google.firebase.appdistribution { } -package com.google.firebase.appdistribution.ktx { - - public final class FirebaseAppDistributionKt { - method @Deprecated public static operator com.google.firebase.appdistribution.BinaryType component1(com.google.firebase.appdistribution.AppDistributionRelease); - method @Deprecated public static operator long component1(com.google.firebase.appdistribution.UpdateProgress); - method @Deprecated public static operator String component2(com.google.firebase.appdistribution.AppDistributionRelease); - method @Deprecated public static operator long component2(com.google.firebase.appdistribution.UpdateProgress); - method @Deprecated public static operator long component3(com.google.firebase.appdistribution.AppDistributionRelease); - method @Deprecated public static operator com.google.firebase.appdistribution.UpdateStatus component3(com.google.firebase.appdistribution.UpdateProgress); - method @Deprecated public static operator String? component4(com.google.firebase.appdistribution.AppDistributionRelease); - method @Deprecated public static com.google.firebase.appdistribution.FirebaseAppDistribution getAppDistribution(com.google.firebase.ktx.Firebase); - } - -} - diff --git a/firebase-appdistribution-api/firebase-appdistribution-api.gradle b/firebase-appdistribution-api/firebase-appdistribution-api.gradle index ed9f7d9f63c..5a097ded575 100644 --- a/firebase-appdistribution-api/firebase-appdistribution-api.gradle +++ b/firebase-appdistribution-api/firebase-appdistribution-api.gradle @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id 'firebase-library' id("kotlin-android") @@ -29,9 +31,9 @@ firebaseLibrary { android { namespace "com.google.firebase.appdistribution" compileSdkVersion project.compileSdkVersion - + defaultConfig { - minSdkVersion 21 + minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion multiDexEnabled true versionName version @@ -41,9 +43,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } testOptions { unitTests { includeAndroidResources = true @@ -51,11 +50,12 @@ android { } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { api libs.playservices.tasks - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-common-ktx:21.0.0") - api("com.google.firebase:firebase-components:18.0.0") + api(libs.firebase.common) + api(libs.firebase.components) implementation libs.androidx.annotation implementation libs.kotlin.stdlib @@ -69,7 +69,6 @@ dependencies { testImplementation libs.truth testImplementation libs.junit testImplementation libs.mockito.core - testImplementation libs.mockito.mockito.inline testImplementation libs.robolectric androidTestImplementation libs.androidx.test.core diff --git a/firebase-appdistribution-api/gradle.properties b/firebase-appdistribution-api/gradle.properties index a39a1d388f4..423f04cce3d 100644 --- a/firebase-appdistribution-api/gradle.properties +++ b/firebase-appdistribution-api/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta16 -latestReleasedVersion=16.0.0-beta15 +version=16.0.0-beta18 +latestReleasedVersion=16.0.0-beta17 diff --git a/firebase-appdistribution-api/ktx/api.txt b/firebase-appdistribution-api/ktx/api.txt deleted file mode 100644 index da4f6cc18fe..00000000000 --- a/firebase-appdistribution-api/ktx/api.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 3.0 diff --git a/firebase-appdistribution-api/ktx/gradle.properties b/firebase-appdistribution-api/ktx/gradle.properties deleted file mode 100644 index 9eff84e6c72..00000000000 --- a/firebase-appdistribution-api/ktx/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -android.enableUnitTestBinaryResources=true diff --git a/firebase-appdistribution-api/ktx/ktx.gradle b/firebase-appdistribution-api/ktx/ktx.gradle deleted file mode 100644 index 12398b0347c..00000000000 --- a/firebase-appdistribution-api/ktx/ktx.gradle +++ /dev/null @@ -1,73 +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. - -plugins { - id 'firebase-library' - id("kotlin-android") -} - -firebaseLibrary { - libraryGroup = "appdistribution" - testLab.enabled = true - publishJavadoc = false - releaseNotes { - enabled.set(false) - } - previewMode = "beta" -} - -android { - namespace "com.google.firebase.appdistribution.ktx" - compileSdkVersion project.compileSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - 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' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - api(project(":firebase-appdistribution-api")) - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-common-ktx:21.0.0") - - implementation("com.google.firebase:firebase-components:18.0.0") - - testImplementation libs.truth - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation libs.robolectric - - androidTestImplementation libs.androidx.test.core - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation libs.truth - androidTestImplementation 'junit:junit:4.12' -} diff --git a/firebase-appdistribution-api/ktx/src/androidTest/AndroidManifest.xml b/firebase-appdistribution-api/ktx/src/androidTest/AndroidManifest.xml deleted file mode 100644 index b8b4e405e38..00000000000 --- a/firebase-appdistribution-api/ktx/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - 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 deleted file mode 100644 index a818396f7c6..00000000000 --- a/firebase-appdistribution-api/ktx/src/androidTest/kotlin/com/google/firebase/app/distribution/ktx/FirebaseAppDistributionTests.kt +++ /dev/null @@ -1,129 +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.appdistribution.ktx - -import androidx.test.core.app.ApplicationProvider -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.appdistribution.AppDistributionRelease -import com.google.firebase.appdistribution.BinaryType -import com.google.firebase.appdistribution.FirebaseAppDistribution -import com.google.firebase.appdistribution.UpdateProgress -import com.google.firebase.appdistribution.UpdateStatus -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app -import com.google.firebase.ktx.initialize -import com.google.firebase.platforminfo.UserAgentPublisher -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -const val APP_ID = "APP_ID" -const val API_KEY = "API_KEY" - -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() - } -} - -@RunWith(AndroidJUnit4ClassRunner::class) -class FirebaseAppDistributionTests : BaseTestCase() { - @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" - - override fun getVersionCode(): Long = 1L - - override fun getReleaseNotes(): String = "Changelog..." - - override fun getBinaryType(): BinaryType = BinaryType.AAB - } - - 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) - } - - @Test - fun updateProgressDestructuringDeclarationsWork() { - val mockUpdateProgress = - object : UpdateProgress { - override fun getApkBytesDownloaded(): Long = 1200L - - override fun getApkFileTotalBytes(): Long = 9000L - - override fun getUpdateStatus(): UpdateStatus = UpdateStatus.DOWNLOADING - } - - val (downloaded, total, status) = mockUpdateProgress - - 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) - } -} diff --git a/firebase-appdistribution-api/ktx/src/main/AndroidManifest.xml b/firebase-appdistribution-api/ktx/src/main/AndroidManifest.xml deleted file mode 100644 index 45c1bb0580b..00000000000 --- a/firebase-appdistribution-api/ktx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/Logging.kt b/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/Logging.kt deleted file mode 100644 index e0e7e27519b..00000000000 --- a/firebase-appdistribution-api/ktx/src/main/kotlin/com/google/firebase/appdistribution/ktx/Logging.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 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.appdistribution.ktx - -import androidx.annotation.Keep -import com.google.firebase.appdistribution.BuildConfig -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.platforminfo.LibraryVersionComponent - -internal const val LIBRARY_NAME: String = "fire-appdistribution-ktx" - -/** @suppress */ -@Keep -class FirebaseAppdistributionApiLegacyRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) - } -} diff --git a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt b/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt deleted file mode 100644 index ccee214d131..00000000000 --- a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt +++ /dev/null @@ -1,169 +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.appdistribution.ktx - -import androidx.annotation.Keep -import com.google.firebase.FirebaseApp -import com.google.firebase.appdistribution.AppDistributionRelease -import com.google.firebase.appdistribution.FirebaseAppDistribution -import com.google.firebase.appdistribution.UpdateProgress -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.ktx.Firebase - -/** - * Accessing this object for Kotlin apps has changed; see the - * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). - * - * Returns the [FirebaseAppDistribution] instance of the default [FirebaseApp]. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -val Firebase.appDistribution: FirebaseAppDistribution - get() = FirebaseAppDistribution.getInstance() - -/** - * Destructuring declaration for [AppDistributionRelease] to provide binaryType. - * - * @return the binaryType of the [AppDistributionRelease] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppDistributionRelease.component1() = binaryType - -/** - * Destructuring declaration for [AppDistributionRelease] to provide displayVersion. - * - * @return the displayVersion of the [AppDistributionRelease] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppDistributionRelease.component2() = displayVersion - -/** - * Destructuring declaration for [AppDistributionRelease] to provide versionCode. - * - * @return the versionCode of the [AppDistributionRelease] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppDistributionRelease.component3() = versionCode - -/** - * Destructuring declaration for [AppDistributionRelease] to provide releaseNotes. - * - * @return the releaseNotes of the [AppDistributionRelease] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun AppDistributionRelease.component4() = releaseNotes - -/** - * Destructuring declaration for [UpdateProgress] to provide apkBytesDownloaded. - * - * @return the apkBytesDownloaded of the [UpdateProgress] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun UpdateProgress.component1() = apkBytesDownloaded - -/** - * Destructuring declaration for [UpdateProgress] to provide apkFileTotalBytes. - * - * @return the apkFileTotalBytes of the [UpdateProgress] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun UpdateProgress.component2() = apkFileTotalBytes - -/** - * Destructuring declaration for [UpdateProgress] to provide updateStatus. - * - * @return the updateStatus of the [UpdateProgress] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun UpdateProgress.component3() = updateStatus - -/** - * @suppress - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-appdistribution-api-ktx` are now deprecated. As early as April - * 2024, we'll no longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -@Keep -class FirebaseAppDistributionKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = listOf() -} diff --git a/firebase-appdistribution/CHANGELOG.md b/firebase-appdistribution/CHANGELOG.md index c7ef8338657..fa9f7c8f02d 100644 --- a/firebase-appdistribution/CHANGELOG.md +++ b/firebase-appdistribution/CHANGELOG.md @@ -1,88 +1,100 @@ # Unreleased +# 16.0.0-beta17 + +- [changed] Bumped internal dependencies. + +# 16.0.0-beta16 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. # 16.0.0-beta15 -* [fixed] Added custom tab support for more browsers [#6692] + +- [fixed] Added custom tab support for more browsers [#6692] # 16.0.0-beta14 -* [changed] Internal improvements to testing on Android 14 + +- [changed] Internal improvements to testing on Android 14 # 16.0.0-beta13 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 16.0.0-beta12 -* [changed] Bump internal dependencies. + +- [changed] Bump internal dependencies. # 16.0.0-beta10 -* [fixed] Updated the third-party license file to include Dagger's license. + +- [fixed] Updated the third-party license file to include Dagger's license. # 16.0.0-beta09 -* [feature] Improved development mode to allow all API calls to be made without having to sign in. + +- [feature] Improved development mode to allow all API calls to be made without having to sign in. # 16.0.0-beta08 -* [fixed] Fixed an issue where a crash happened whenever a feedback - notification was shown on devices running Android 4.4 and lower. + +- [fixed] Fixed an issue where a crash happened whenever a feedback notification was shown on + devices running Android 4.4 and lower. # 16.0.0-beta07 -* [feature] Added support for testers to attach JPEG screenshots to their - feedback. + +- [feature] Added support for testers to attach JPEG screenshots to their feedback. # 16.0.0-beta06 -* [feature] Added support for in-app tester feedback. To learn more, see + +- [feature] Added support for in-app tester feedback. To learn more, see [Collect feedback from testers](/docs/app-distribution/collect-feedback-from-testers). -* [fixed] Fixed a bug where only the last listener added to an `UpdateTask` - using `addOnProgressListener()` would receive updates. +- [fixed] Fixed a bug where only the last listener added to an `UpdateTask` using + `addOnProgressListener()` would receive updates. # 16.0.0-beta05 -* [unchanged] Updated to accommodate the release of the updated - [appdistro] Kotlin extensions library. + +- [unchanged] Updated to accommodate the release of the updated [appdistro] Kotlin extensions + library. # 16.0.0-beta03 -* [feature] The [appdistro] SDK has been split into two libraries: - * `firebase-appdistribution-api` - The API-only library
- This new API-only library is functional only when the full - [appdistro] SDK implementation (`firebase-appdistribution`) is present. - `firebase-appdistribution-api` can be included in all +- [feature] The [appdistro] SDK has been split into two libraries: + + - `firebase-appdistribution-api` - The API-only library
This new API-only library is + functional only when the full [appdistro] SDK implementation (`firebase-appdistribution`) is + present. `firebase-appdistribution-api` can be included in all [build variants](https://developer.android.com/studio/build/build-variants){: .external}. - * `firebase-appdistribution` - The full SDK implementation
- This full SDK implementation is optional and should only be included in - pre-release builds. + - `firebase-appdistribution` - The full SDK implementation
This full SDK implementation is + optional and should only be included in pre-release builds. Visit the documentation to learn how to - [add these SDKs](/docs/app-distribution/set-up-alerts?platform=android#add-appdistro) - to your Android app. - + [add these SDKs](/docs/app-distribution/set-up-alerts?platform=android#add-appdistro) to your + Android app. ## Kotlin -* [removed] The Kotlin extensions library `firebase-appdistribution-ktx` - has been removed. All its functionality has been moved to the new API-only - library: `firebase-appdistribution-api-ktx`. + +- [removed] The Kotlin extensions library `firebase-appdistribution-ktx` has been removed. All its + functionality has been moved to the new API-only library: `firebase-appdistribution-api-ktx`. # 16.0.0-beta02 -* [fixed] Fixed a bug that prevented testers from signing in when the app had -an underscore in the package name. -* [fixed] Fixed a UI bug where the APK download notification displayed the -incorrect error message. -* [changed] Internal improvements to tests. +- [fixed] Fixed a bug that prevented testers from signing in when the app had an underscore in the + package name. +- [fixed] Fixed a UI bug where the APK download notification displayed the incorrect error message. +- [changed] Internal improvements to tests. ## Kotlin -The Kotlin extensions library transitively includes the base -`firebase-app-distribution` library. The Kotlin extensions library has no -additional updates. + +The Kotlin extensions library transitively includes the base `firebase-app-distribution` library. +The Kotlin extensions library has no additional updates. # 16.0.0-beta01 -* [feature] The [appdistro] Android SDK is now available in beta. You - can use this SDK to notify testers in-app when a new test build is available. - To learn more, visit the - [[appdistro] reference documentation](/docs/reference/android/com/google/firebase/appdistribution/package-summary). +- [feature] The [appdistro] Android SDK is now available in beta. You can use this SDK to notify + testers in-app when a new test build is available. To learn more, visit the + [[appdistro] reference documentation](/docs/reference/android/com/google/firebase/appdistribution/package-summary). ## Kotlin -The [appdistro] Android library with Kotlin extensions is now available in -beta. The Kotlin extensions library transitively includes the base -`firebase-app-distribution` library. To learn more, visit the -[[appdistro] KTX reference documentation](/docs/reference/kotlin/com/google/firebase/appdistribution/ktx/package-summary). +The [appdistro] Android library with Kotlin extensions is now available in beta. The Kotlin +extensions library transitively includes the base `firebase-app-distribution` library. To learn +more, visit the +[[appdistro] KTX reference documentation](/docs/reference/kotlin/com/google/firebase/appdistribution/ktx/package-summary). diff --git a/firebase-appdistribution/firebase-appdistribution.gradle b/firebase-appdistribution/firebase-appdistribution.gradle index 55a5a09884e..9239700d1ac 100644 --- a/firebase-appdistribution/firebase-appdistribution.gradle +++ b/firebase-appdistribution/firebase-appdistribution.gradle @@ -24,7 +24,6 @@ firebaseLibrary { releaseNotes { name.set("{{appdistro}}") versionName.set("app-distro") - hasKTX.set(false) } } @@ -54,7 +53,8 @@ android { } thirdPartyLicenses { - add 'Dagger', "${rootDir}/third_party/licenses/apache-2.0.txt" + add 'Apache-2.0', "${rootDir}/third_party/licenses/apache-2.0.txt" + add 'Dagger', "${rootDir}/third_party/licenses/dagger.txt" } dependencies { @@ -62,12 +62,12 @@ dependencies { exclude group: "javax.inject", module: "javax.inject" } - api("com.google.firebase:firebase-appdistribution-api:16.0.0-beta11") { + api(project(":firebase-appdistribution-api")) { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' } - api 'com.google.firebase:firebase-common:21.0.0' - api 'com.google.firebase:firebase-components:18.0.0' + api libs.firebase.common + api libs.firebase.components api('com.google.firebase:firebase-installations-interop:17.1.0') { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' @@ -82,8 +82,9 @@ dependencies { compileOnly libs.autovalue.annotations - runtimeOnly('com.google.firebase:firebase-installations:17.1.3') { + runtimeOnly('com.google.firebase:firebase-installations:18.0.0') { exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-common-ktx' exclude group: 'com.google.firebase', module: 'firebase-components' } @@ -98,7 +99,7 @@ dependencies { testImplementation libs.androidx.test.core testImplementation libs.truth testImplementation libs.junit - testImplementation libs.mockito.mockito.inline + testImplementation libs.mockito.core testImplementation libs.robolectric androidTestImplementation(project(":integ-testing")){ @@ -111,5 +112,4 @@ dependencies { androidTestImplementation libs.truth androidTestImplementation libs.junit androidTestImplementation libs.mockito.core - androidTestImplementation libs.mockito.mockito.inline } diff --git a/firebase-appdistribution/gradle.properties b/firebase-appdistribution/gradle.properties index 5dcfcbcc66c..4f3941a13b3 100644 --- a/firebase-appdistribution/gradle.properties +++ b/firebase-appdistribution/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.0-beta16 -latestReleasedVersion=16.0.0-beta15 +version=16.0.0-beta18 +latestReleasedVersion=16.0.0-beta17 diff --git a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TaskCache.java b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TaskCache.java index b370aea77de..ee99cd8eb09 100644 --- a/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TaskCache.java +++ b/firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TaskCache.java @@ -14,6 +14,7 @@ package com.google.firebase.appdistribution.impl; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.annotations.concurrent.Lightweight; @@ -66,6 +67,20 @@ Task getOrCreateTask(TaskProducer producer) { return taskCompletionSource.getTask(); } + /** + * Returns a task that completes when this object's internal sequential executor has finished + * processing all enqueued operations. + *

+ * This method should never be called in production code; however, it is useful in unit tests to + * ensure deterministic behavior. + */ + @VisibleForTesting + Task newTaskForSequentialExecutorFlush() { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + sequentialExecutor.execute(() -> taskCompletionSource.setResult(null)); + return taskCompletionSource.getTask(); + } + private static boolean isOngoing(Task task) { return task != null && !task.isComplete(); } diff --git a/firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/NewReleaseFetcherTest.java b/firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/NewReleaseFetcherTest.java index 84e67c747eb..df69e582615 100644 --- a/firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/NewReleaseFetcherTest.java +++ b/firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/NewReleaseFetcherTest.java @@ -112,6 +112,7 @@ public void checkForNewRelease_whenCalledMultipleTimes_onlyFetchesReleasesOnce() // Don't set the result until after calling twice, to make sure that the task from the first // call is still ongoing. + awaitTask(newReleaseFetcher.cachedCheckForNewRelease.newTaskForSequentialExecutorFlush()); task.setResult(null); awaitTask(checkForNewReleaseTask1); awaitTask(checkForNewReleaseTask2); diff --git a/firebase-appdistribution/test-app/README.md b/firebase-appdistribution/test-app/README.md index 2d630d40e0e..179296e4d64 100644 --- a/firebase-appdistribution/test-app/README.md +++ b/firebase-appdistribution/test-app/README.md @@ -2,26 +2,33 @@ ## Setup -Download the `google-services.json` file from [Firebase Console](https://console.firebase.google.com/) -(for whatever Firebase project you have or want to integrate the `dev-app`) and store it under the -current directory. +Download the `google-services.json` file from +[Firebase Console](https://console.firebase.google.com/) (for whatever Firebase project you have or +want to integrate the `dev-app`) and store it under the current directory.

> **Note:** The [Package name](https://firebase.google.com/docs/android/setup#register-app) for your -app created on the Firebase Console (for which the `google-services.json` is downloaded) must match -the [applicationId](https://developer.android.com/studio/build/application-id.html) declared in the -`test-app/test-app.gradle` for the app to link to Firebase. +> app created on the Firebase Console (for which the `google-services.json` is downloaded) must +> match the [applicationId](https://developer.android.com/studio/build/application-id.html) declared +> in the `test-app/test-app.gradle` for the app to link to Firebase. ## Build & Install -### Enable the test-app as a subproject ### +### Enable the test-app as a subproject -You'll need to do this on a fresh checkout, otherwise you will see the error `Project 'test-app' not found in project ':firebase-appdistribution'.` when running `./gradlew` tasks for the test app. +You'll need to do this on a fresh checkout, otherwise you will see the error +`Project 'test-app' not found in project ':firebase-appdistribution'.` when running `./gradlew` +tasks for the test app. -By default, product-specific subprojects are disabled in the SDK because their `google-services.json` files aren't always available in CI and therefore they can't be reliably built. To do local development with this test app, it needs to be manually enabled by uncommenting it out at the bottom of [subprojects.cfg](https://github.com/firebase/firebase-android-sdk/blob/main/subprojects.cfg) (*Don't check this in*) +By default, product-specific subprojects are disabled in the SDK because their +`google-services.json` files aren't always available in CI and therefore they can't be reliably +built. To do local development with this test app, it needs to be manually enabled by uncommenting +it out at the bottom of +[subprojects.cfg](https://github.com/firebase/firebase-android-sdk/blob/main/subprojects.cfg) +(_Don't check this in_) ``` # @@ -42,8 +49,9 @@ firebase-appdistribution:test-app firebase-android-sdk$ ./gradlew :clean :firebase-appdistribution:test-app:build ``` -After the build is successful, [bring up emulator/physical device](https://developer.android.com/studio/run/emulator) -and install the apk: +After the build is successful, +[bring up emulator/physical device](https://developer.android.com/studio/run/emulator) and install +the apk: ``` firebase-android-sdk$ adb install firebase-appdistribution/test-app/build/outputs/apk/release/test-app-release.apk @@ -51,26 +59,32 @@ firebase-android-sdk$ adb install firebase-appdistribution/test-app/build/output ## Test In-App Feedback Locally -In-App Feedback is currently tricky to test locally because it relies on the -fact that a release exists with the same hash of the running binary. +In-App Feedback is currently tricky to test locally because it relies on the fact that a release +exists with the same hash of the running binary. To build the debug APK, upload it to App Distribution, and install it on the running emulator: -1. In firebase-appdistribution/test-app/test-app.gradle, uncomment the line `// testers "your email here"` and replace "your email here" with the email you intend to use for testing. + +1. In firebase-appdistribution/test-app/test-app.gradle, uncomment the line + `// testers "your email here"` and replace "your email here" with the email you intend to use for + testing. 1. Start an emulator 1. Run the following command from the repo's root directory: - ``` - ./gradlew clean :firebase-appdistribution:test-app:build :firebase-appdistribution:test-app:appDistributionUploadBetaDebug && adb install firebase-appdistribution/test-app/build/outputs/apk/beta/debug/test-app-beta-debug.apk - ``` - -This will build the debug variant of the app (which has the full SDK), upload it to App Distribution, and install it on the running emulator. Run the app in the emulator to test. As an alternative you can always download it using App Distribution, but using `adb` is just faster. + ``` + ./gradlew clean :firebase-appdistribution:test-app:build :firebase-appdistribution:test-app:appDistributionUploadBetaDebug && adb install firebase-appdistribution/test-app/build/outputs/apk/beta/debug/test-app-beta-debug.apk + ``` + +This will build the debug variant of the app (which has the full SDK), upload it to App +Distribution, and install it on the running emulator. Run the app in the emulator to test. As an +alternative you can always download it using App Distribution, but using `adb` is just faster. After that, if you want to avoid having to do this every time you want to test locally: 1. Submit feedback in the locally running app, to generate some logs -1. In the Logcat output, find the release name (i.e. `"projects/1095562444941/installations/fCmpB677QTybkwfKbViGI-/releases/3prs96fui9kb0"`) +1. In the Logcat output, find the release name (i.e. + `"projects/1095562444941/installations/fCmpB677QTybkwfKbViGI-/releases/3prs96fui9kb0"`) 1. Modify the body of `ReleaseIdentifier.identifyRelease()` to be: - ``` - return Tasks.forResult(""); - ``` + ``` + return Tasks.forResult(""); + ``` diff --git a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/CustomNotificationFeedbackTrigger.kt b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/CustomNotificationFeedbackTrigger.kt index d8690b2e73b..02d6921d3d2 100644 --- a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/CustomNotificationFeedbackTrigger.kt +++ b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/CustomNotificationFeedbackTrigger.kt @@ -31,8 +31,8 @@ import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import com.google.firebase.appdistribution.ktx.appDistribution -import com.google.firebase.ktx.Firebase +import com.google.firebase.appdistribution.appDistribution +import com.google.firebase.Firebase import java.io.IOException import com.google.firebase.appdistribution.testapp.R diff --git a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/MainActivity.kt b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/MainActivity.kt index 279361a0d47..9e63c387eac 100644 --- a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/MainActivity.kt +++ b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/MainActivity.kt @@ -42,9 +42,9 @@ import com.google.android.material.textfield.TextInputLayout import com.google.firebase.appdistribution.AppDistributionRelease import com.google.firebase.appdistribution.InterruptionLevel import com.google.firebase.appdistribution.UpdateProgress -import com.google.firebase.appdistribution.ktx.appDistribution +import com.google.firebase.appdistribution.appDistribution import com.google.firebase.appdistribution.testapp.BuildConfig -import com.google.firebase.ktx.Firebase +import com.google.firebase.Firebase import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import com.google.firebase.appdistribution.testapp.R diff --git a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ScreenshotDetectionFeedbackTrigger.kt b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ScreenshotDetectionFeedbackTrigger.kt index c904ed1e507..1b031306a11 100644 --- a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ScreenshotDetectionFeedbackTrigger.kt +++ b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ScreenshotDetectionFeedbackTrigger.kt @@ -28,8 +28,8 @@ import android.provider.MediaStore import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat -import com.google.firebase.appdistribution.ktx.appDistribution -import com.google.firebase.ktx.Firebase +import com.google.firebase.appdistribution.appDistribution +import com.google.firebase.Firebase import java.util.* class ScreenshotDetectionFeedbackTrigger( diff --git a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt index 950d65b096e..b73845fbd1d 100644 --- a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt +++ b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/SecondActivity.kt @@ -23,8 +23,8 @@ import android.view.MenuInflater import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatButton -import com.google.firebase.appdistribution.ktx.appDistribution -import com.google.firebase.ktx.Firebase +import com.google.firebase.appdistribution.appDistribution +import com.google.firebase.Firebase import com.google.firebase.appdistribution.testapp.R class SecondActivity : AppCompatActivity() { diff --git a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ShakeDetectionFeedbackTrigger.kt b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ShakeDetectionFeedbackTrigger.kt index 5481ccd603d..5d0bb7722e5 100644 --- a/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ShakeDetectionFeedbackTrigger.kt +++ b/firebase-appdistribution/test-app/src/main/kotlin/com/googletest/firebase/appdistribution/testapp/ShakeDetectionFeedbackTrigger.kt @@ -21,8 +21,8 @@ import android.app.Application import android.hardware.SensorManager import android.os.Bundle import android.util.Log -import com.google.firebase.appdistribution.ktx.appDistribution -import com.google.firebase.ktx.Firebase +import com.google.firebase.appdistribution.appDistribution +import com.google.firebase.Firebase import com.squareup.seismic.ShakeDetector import com.google.firebase.appdistribution.testapp.R diff --git a/firebase-appdistribution/test-app/test-app.gradle b/firebase-appdistribution/test-app/test-app.gradle index 420224b9e78..0498bd56bb7 100644 --- a/firebase-appdistribution/test-app/test-app.gradle +++ b/firebase-appdistribution/test-app/test-app.gradle @@ -68,11 +68,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { // TODO(rachelprince): Add flag to build with public version of SDK println("Building with HEAD version ':firebase-appdistribution' of SDK") @@ -82,7 +81,6 @@ dependencies { // In this test project we also need to explicitly declare these dependencies implementation project(':firebase-appdistribution-api') // All variants use the API - implementation project(':firebase-appdistribution-api:ktx') implementation "androidx.activity:activity-ktx:1.6.0" implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -91,7 +89,6 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.5.3" implementation "com.google.android.gms:play-services-tasks:18.0.2" implementation 'com.google.android.material:material:1.6.1' - implementation 'com.google.firebase:firebase-common-ktx:21.0.0' // Shake detection implementation 'com.squareup:seismic:1.0.3' // Other dependencies diff --git a/firebase-common/CHANGELOG.md b/firebase-common/CHANGELOG.md index 8015035242a..ed9082936bd 100644 --- a/firebase-common/CHANGELOG.md +++ b/firebase-common/CHANGELOG.md @@ -1,25 +1,43 @@ # Unreleased -* [fixed] Correctly declare dependency on firebase-components, issue #5732 -* [changed] Added extension method `Random.nextAlphanumericString()` (PR #5818) + +# 22.0.1 + +- [changed] Improve datastore support (#7277) + +# 22.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. +- [removed] **Breaking Change**: Stopped releasing the deprecated Kotlin extensions (KTX) module and + removed it from the Firebase Android BoM. Instead, use the KTX APIs from the main module. For + details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration). + +# 21.0.0 + +- [fixed] Correctly declare dependency on firebase-components, issue #5732 +- [changed] Added extension method `Random.nextAlphanumericString()` (PR #5818) +- [changed] Migrated internal `SharedPreferences` usages to `DataStore`. + ([GitHub PR #6801](https://github.com/firebase/firebase-android-sdk/pull/6801){ .external}) # 20.4.0 -* [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-common-ktx` -to `com.google.firebase:firebase-common` under the `com.google.firebase` package. -For details, see the -[FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) + +- [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-common-ktx` to + `com.google.firebase:firebase-common` under the `com.google.firebase` package. For details, see + the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) ## Kotlin -* [deprecated] All the APIs from `com.google.firebase:firebase-common-ktx` have been added to -`com.google.firebase:firebase-common` under the `com.google.firebase package`, and all the -Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-common-ktx` are now deprecated. -As early as April 2024, we'll no longer release KTX modules. For details, see the -FAQ about this initiative. -[FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) + +- [deprecated] All the APIs from `com.google.firebase:firebase-common-ktx` have been added to + `com.google.firebase:firebase-common` under the `com.google.firebase package`, and all the Kotlin + extensions (KTX) APIs in `com.google.firebase:firebase-common-ktx` are now deprecated. As early as + April 2024, we'll no longer release KTX modules. For details, see the FAQ about this initiative. + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) # 20.3.3 -* [fixed] Addressed issue with C++ being absent in user agent. + +- [fixed] Addressed issue with C++ being absent in user agent. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-common` library. The Kotlin extensions library has no additional -updates + +The Kotlin extensions library transitively includes the updated `firebase-common` library. The +Kotlin extensions library has no additional updates diff --git a/firebase-common/README.md b/firebase-common/README.md index 63ae423d0cd..3b3cea4b37a 100644 --- a/firebase-common/README.md +++ b/firebase-common/README.md @@ -1,11 +1,12 @@ # Firebase Common -firebase-common contains the FirebaseApp, which is used to configure -the firebase sdks as well as the infrastructure that firebase sdks use -to discover and interact with other firebase sdks. + +firebase-common contains the FirebaseApp, which is used to configure the firebase sdks as well as +the infrastructure that firebase sdks use to discover and interact with other firebase sdks. ## Running tests. Unit tests can be run by + ``` $ ./gradlew :firebase-common:check ``` diff --git a/firebase-common/api.txt b/firebase-common/api.txt index 57f4f898a25..4da45aa2621 100644 --- a/firebase-common/api.txt +++ b/firebase-common/api.txt @@ -82,23 +82,6 @@ package com.google.firebase { } -package com.google.firebase.ktx { - - @Deprecated public final class Firebase { - field @Deprecated public static final com.google.firebase.ktx.Firebase INSTANCE; - } - - public final class FirebaseKt { - method @Deprecated public static com.google.firebase.FirebaseApp app(com.google.firebase.ktx.Firebase, String name); - method @Deprecated public static com.google.firebase.FirebaseApp getApp(com.google.firebase.ktx.Firebase); - method @Deprecated public static com.google.firebase.FirebaseOptions getOptions(com.google.firebase.ktx.Firebase); - method @Deprecated public static com.google.firebase.FirebaseApp? initialize(com.google.firebase.ktx.Firebase, android.content.Context context); - method @Deprecated public static com.google.firebase.FirebaseApp initialize(com.google.firebase.ktx.Firebase, android.content.Context context, com.google.firebase.FirebaseOptions options); - method @Deprecated public static com.google.firebase.FirebaseApp initialize(com.google.firebase.ktx.Firebase, android.content.Context context, com.google.firebase.FirebaseOptions options, String name); - } - -} - package com.google.firebase.provider { public class FirebaseInitProvider extends android.content.ContentProvider { diff --git a/firebase-common/data-collection-tests/data-collection-tests.gradle.kts b/firebase-common/data-collection-tests/data-collection-tests.gradle.kts index ad1285a8cba..b84eb30f426 100644 --- a/firebase-common/data-collection-tests/data-collection-tests.gradle.kts +++ b/firebase-common/data-collection-tests/data-collection-tests.gradle.kts @@ -34,8 +34,8 @@ android { } dependencies { - implementation("com.google.firebase:firebase-common:21.0.0") - implementation("com.google.firebase:firebase-components:18.0.0") + implementation("com.google.firebase:firebase-common:22.0.0") + implementation(libs.firebase.components) implementation(platform(libs.kotlin.bom)) testImplementation(libs.androidx.core) diff --git a/firebase-common/data-collection-tests/src/test/java/com/google/firebase/DataCollectionPreNDefaultDisabledTest.java b/firebase-common/data-collection-tests/src/test/java/com/google/firebase/DataCollectionPreNDefaultDisabledTest.java index 81c71400b7d..f95e249c7cf 100644 --- a/firebase-common/data-collection-tests/src/test/java/com/google/firebase/DataCollectionPreNDefaultDisabledTest.java +++ b/firebase-common/data-collection-tests/src/test/java/com/google/firebase/DataCollectionPreNDefaultDisabledTest.java @@ -20,15 +20,15 @@ import static com.google.firebase.DataCollectionTestUtil.withApp; import android.content.SharedPreferences; -import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.internal.DataCollectionConfigStorage; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; -@RunWith(AndroidJUnit4.class) -@Config(sdk = 21) +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.OLDEST_SDK) public class DataCollectionPreNDefaultDisabledTest { @Test diff --git a/firebase-common/firebase-common.gradle.kts b/firebase-common/firebase-common.gradle.kts index 88bfa58e27d..a406b0b1707 100644 --- a/firebase-common/firebase-common.gradle.kts +++ b/firebase-common/firebase-common.gradle.kts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("firebase-library") id("kotlin-android") @@ -44,7 +46,6 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { jvmTarget = "1.8" } testOptions { targetSdk = targetSdkVersion unitTests { isIncludeAndroidResources = true } @@ -52,11 +53,14 @@ android { lint { targetSdk = targetSdkVersion } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { api(libs.kotlin.coroutines.tasks) - api("com.google.firebase:firebase-components:18.0.0") - api("com.google.firebase:firebase-annotations:16.2.0") + api(libs.firebase.components) + api(libs.firebase.annotations) + implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.annotation) implementation(libs.androidx.futures) implementation(libs.kotlin.stdlib) @@ -82,7 +86,6 @@ dependencies { androidTestImplementation(project(":integ-testing")) { exclude("com.google.firebase", "firebase-common") - exclude("com.google.firebase", "firebase-common-ktx") } // TODO(Remove when FirbaseAppTest has been modernized to use LiveData) diff --git a/firebase-common/gradle.properties b/firebase-common/gradle.properties index b878bad37ff..ad317096e94 100644 --- a/firebase-common/gradle.properties +++ b/firebase-common/gradle.properties @@ -1,3 +1,3 @@ -version=21.0.1 -latestReleasedVersion=21.0.0 +version=22.0.2 +latestReleasedVersion=22.0.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-common/ktx/api.txt b/firebase-common/ktx/api.txt deleted file mode 100644 index da4f6cc18fe..00000000000 --- a/firebase-common/ktx/api.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 3.0 diff --git a/firebase-common/ktx/gradle.properties b/firebase-common/ktx/gradle.properties deleted file mode 100644 index 9eff84e6c72..00000000000 --- a/firebase-common/ktx/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -android.enableUnitTestBinaryResources=true diff --git a/firebase-common/ktx/ktx.gradle.kts b/firebase-common/ktx/ktx.gradle.kts deleted file mode 100644 index 61553f0adeb..00000000000 --- a/firebase-common/ktx/ktx.gradle.kts +++ /dev/null @@ -1,53 +0,0 @@ -// 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 { - libraryGroup = "common" - publishJavadoc = false - releaseNotes { enabled.set(false) } -} - -android { - val compileSdkVersion: Int by rootProject - val targetSdkVersion: Int by rootProject - val minSdkVersion: Int by rootProject - compileSdk = compileSdkVersion - namespace = "com.google.firebase.ktx" - defaultConfig { - minSdk = minSdkVersion - targetSdk = targetSdkVersion - } - sourceSets { - getByName("main") { java.srcDirs("src/main/kotlin") } - getByName("test") { java.srcDirs("src/test/kotlin") } - } - kotlinOptions { jvmTarget = "1.8" } - testOptions.unitTests.isIncludeAndroidResources = true -} - -dependencies { - api(project(":firebase-common")) - implementation("com.google.firebase:firebase-components:18.0.0") - implementation("com.google.firebase:firebase-annotations:16.2.0") - testImplementation(libs.androidx.test.core) - testImplementation(libs.junit) - testImplementation(libs.kotlin.coroutines.test) - testImplementation(libs.robolectric) - testImplementation(libs.truth) -} diff --git a/firebase-common/ktx/src/main/AndroidManifest.xml b/firebase-common/ktx/src/main/AndroidManifest.xml deleted file mode 100644 index 4e04e3fece4..00000000000 --- a/firebase-common/ktx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Logging.kt b/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Logging.kt deleted file mode 100644 index 5ba2cf4d54c..00000000000 --- a/firebase-common/ktx/src/main/kotlin/com/google/firebase/ktx/Logging.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 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.ktx - -import androidx.annotation.Keep -import com.google.firebase.BuildConfig -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.platforminfo.LibraryVersionComponent - -internal const val LIBRARY_NAME: String = "fire-core-ktx" - -/** @suppress */ -@Keep -class FirebaseCommonLegacyRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return listOf(LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)) - } -} 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 deleted file mode 100644 index bc3eeb54cd5..00000000000 --- a/firebase-common/ktx/src/test/kotlin/com/google/firebase/ktx/Tests.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.ktx - -import androidx.test.core.app.ApplicationProvider -import com.google.android.gms.tasks.Tasks -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.platforminfo.UserAgentPublisher -import kotlinx.coroutines.tasks.await -import kotlinx.coroutines.test.runTest -import org.junit.Assert.fail -import org.junit.Test -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() - } -} - -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) - } - } -} - -@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(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#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() - } - } -} - -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. - - @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() - - 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() - } - } -} diff --git a/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java b/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java index f24a56c74c5..43417766dba 100644 --- a/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java +++ b/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java @@ -143,7 +143,7 @@ public void testInitializeApp_shouldPublishVersionForFirebaseCommon() { // After sorting the user agents are expected to be {"fire-android/", "fire-auth/x.y.z", // "fire-core/x.y.z", "test-component/1.2.3"} assertThat(actualUserAgent[0]).contains("android-installer"); - assertThat(actualUserAgent[1]).contains("android-min-sdk/21"); + assertThat(actualUserAgent[1]).contains("android-min-sdk/23"); assertThat(actualUserAgent[2]).contains("android-platform"); assertThat(actualUserAgent[3]).contains("android-target-sdk"); assertThat(actualUserAgent[4]).contains("device-brand"); 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 75063296f19..9b3f8da3b5f 100644 --- a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java +++ b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java @@ -37,7 +37,6 @@ import com.google.android.gms.common.api.internal.BackgroundDetector; import com.google.android.gms.common.internal.Objects; import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.common.util.PlatformVersion; import com.google.android.gms.common.util.ProcessUtils; import com.google.firebase.components.Component; import com.google.firebase.components.ComponentDiscovery; @@ -689,8 +688,7 @@ private static class GlobalBackgroundStateListener new AtomicReference<>(); private static void ensureBackgroundStateListenerRegistered(Context context) { - if (!(PlatformVersion.isAtLeastIceCreamSandwich() - && context.getApplicationContext() instanceof Application)) { + if (!(context.getApplicationContext() instanceof Application)) { return; } Application application = (Application) context.getApplicationContext(); diff --git a/firebase-common/src/main/java/com/google/firebase/datastorage/JavaDataStorage.kt b/firebase-common/src/main/java/com/google/firebase/datastorage/JavaDataStorage.kt new file mode 100644 index 00000000000..8de2b3bae4b --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/datastorage/JavaDataStorage.kt @@ -0,0 +1,249 @@ +/* + * Copyright 2025 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.datastorage + +import android.content.Context +import android.os.Process +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStore +import com.google.firebase.annotations.concurrent.Background +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking + +/** + * Wrapper around [DataStore] for easier migration from `SharedPreferences` in Java code. + * + * Automatically migrates data from any `SharedPreferences` that share the same context and name. + * + * There should only ever be _one_ instance of this class per context and name variant. + * + * Note that most of the methods in this class **block** on the _current_ thread, as to help keep + * parity with existing Java code. Typically, you'd want to dispatch this work to another thread + * like [@Background][Background]. + * + * > Do **NOT** use this _unless_ you're bridging Java code. If you're writing new code, or your + * code is in Kotlin, then you should create your own singleton that uses [DataStore] directly. + * + * Example: + * ```java + * JavaDataStorage heartBeatStorage = new JavaDataStorage(applicationContext, "FirebaseHeartBeat"); + * ``` + * + * @property context The [Context] that this data will be saved under. + * @property name What the storage file should be named. + * + * @hide + */ +class JavaDataStorage(val context: Context, val name: String) { + /** + * Used to ensure that there's only ever one call to [editSync] per thread; as to avoid deadlocks. + */ + private val editLock = ThreadLocal() + + private val Context.dataStore: DataStore by + preferencesDataStore( + name = name, + produceMigrations = { listOf(SharedPreferencesMigration(it, name)) }, + corruptionHandler = + ReplaceFileCorruptionHandler { ex -> + Log.w( + JavaDataStorage::class.simpleName, + "CorruptionException in ${name} DataStore running in process ${Process.myPid()}", + ex + ) + emptyPreferences() + } + ) + + private val dataStore = context.dataStore + + /** + * Get data from the datastore _synchronously_. + * + * Note that if the key is _not_ in the datastore, while the [defaultValue] will be returned + * instead- it will **not** be saved to the datastore; you'll have to manually do that. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * Preferences.Key fireCountKey = PreferencesKeys.longKey("fire-count"); + * assert dataStore.get(fireCountKey, 0L) == 0L; + * + * dataStore.putSync(fireCountKey, 102L); + * assert dataStore.get(fireCountKey, 0L) == 102L; + * ``` + * + * @param key The typed key of the entry to get data for. + * @param defaultValue A value to default to, if the key isn't found. + * + * @see Preferences.getOrDefault + */ + fun getSync(key: Preferences.Key, defaultValue: T): T = runBlocking { + dataStore.data.firstOrNull()?.get(key) ?: defaultValue + } + + /** + * Checks if a key is present in the datastore _synchronously_. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * Preferences.Key fireCountKey = PreferencesKeys.longKey("fire-count"); + * assert !dataStore.contains(fireCountKey); + * + * dataStore.putSync(fireCountKey, 102L); + * assert dataStore.contains(fireCountKey); + * ``` + * + * @param key The typed key of the entry to find. + */ + fun contains(key: Preferences.Key): Boolean = runBlocking { + dataStore.data.firstOrNull()?.contains(key) ?: false + } + + /** + * Sets and saves data in the datastore _synchronously_. + * + * Existing values will be overwritten. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * dataStore.putSync(PreferencesKeys.longKey("fire-count"), 102L); + * ``` + * + * @param key The typed key of the entry to save the data under. + * @param value The data to save. + * + * @return The [Preferences] object that the data was saved under. + */ + fun putSync(key: Preferences.Key, value: T): Preferences = runBlocking { + dataStore.edit { it[key] = value } + } + + /** + * Gets all data in the datastore _synchronously_. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * ArrayList allDates = new ArrayList<>(); + * + * for (Map.Entry, Object> entry : dataStore.getAllSync().entrySet()) { + * if (entry.getValue() instanceof Set) { + * Set dates = new HashSet<>((Set) entry.getValue()); + * if (!dates.isEmpty()) { + * allDates.add(new ArrayList<>(dates)); + * } + * } + * } + * ``` + * + * @return An _immutable_ map of data currently present in the datastore. + */ + fun getAllSync(): Map, Any> = runBlocking { + dataStore.data.firstOrNull()?.asMap() ?: emptyMap() + } + + /** + * Transactionally edit data in the datastore _synchronously_. + * + * Edits made within the [transform] callback will be saved (committed) all at once once the + * [transform] block exits. + * + * Because of the blocking nature of this function, you should _never_ call [editSync] within an + * already running [transform] block. Since this can cause a deadlock, [editSync] will instead + * throw an exception if it's caught. + * + * Blocks on the currently running thread. + * + * Example: + * ```java + * dataStore.editSync((pref) -> { + * Long heartBeatCount = pref.get(HEART_BEAT_COUNT_TAG); + * if (heartBeatCount == null || heartBeatCount > 30) { + * heartBeatCount = 0L; + * } + * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + * pref.set(LAST_STORED_DATE, "1970-0-1"); + * + * return null; + * }); + * ``` + * + * @param transform A callback to invoke with the [MutablePreferences] object. + * + * @return The [Preferences] object that the data was saved under. + * @throws IllegalStateException If you attempt to call [editSync] within another [transform] + * block. + * + * @see Preferences.getOrDefault + */ + fun editSync(transform: (MutablePreferences) -> Unit): Preferences = runBlocking { + if (editLock.get() == true) { + throw IllegalStateException( + """ + Don't call JavaDataStorage.edit() from within an existing edit() callback. + This causes deadlocks, and is generally indicative of a code smell. + Instead, either pass around the initial `MutablePreferences` instance, or don't do everything in a single callback. + """ + .trimIndent() + ) + } + editLock.set(true) + try { + dataStore.edit { transform(it) } + } finally { + editLock.set(false) + } + } +} + +/** + * Helper method for getting the value out of a [Preferences] object if it exists, else falling back + * to the default value. + * + * This is primarily useful when working with an instance of [MutablePreferences] + * - like when working within an [JavaDataStorage.editSync] callback. + * + * Example: + * ```java + * dataStore.editSync((pref) -> { + * long heartBeatCount = DataStoreKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L); + * heartBeatCount+=1; + * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + * + * return null; + * }); + * ``` + * + * @param key The typed key of the entry to get data for. + * @param defaultValue A value to default to, if the key isn't found. + */ +fun Preferences.getOrDefault(key: Preferences.Key, defaultValue: T) = + get(key) ?: defaultValue diff --git a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/package-info.java b/firebase-common/src/main/java/com/google/firebase/datastorage/package-info.java similarity index 87% rename from firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/package-info.java rename to firebase-common/src/main/java/com/google/firebase/datastorage/package-info.java index 1c6d93409f8..44489b704ff 100644 --- a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/internal/package-info.java +++ b/firebase-common/src/main/java/com/google/firebase/datastorage/package-info.java @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,4 +13,4 @@ // limitations under the License. /** @hide */ -package com.google.firebase.dynamiclinks.internal; +package com.google.firebase.datastorage; 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 ff9ba5123fc..7aa8cfb613d 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 @@ -26,6 +26,7 @@ import com.google.firebase.annotations.concurrent.Background; import com.google.firebase.components.Component; import com.google.firebase.components.Dependency; +import com.google.firebase.components.Lazy; import com.google.firebase.components.Qualified; import com.google.firebase.inject.Provider; import com.google.firebase.platforminfo.UserAgentPublisher; @@ -116,7 +117,7 @@ private DefaultHeartBeatController( Provider userAgentProvider, Executor backgroundExecutor) { this( - () -> new HeartBeatInfoStorage(context, persistenceKey), + new Lazy<>(() -> new HeartBeatInfoStorage(context, persistenceKey)), consumers, backgroundExecutor, userAgentProvider, diff --git a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java index a8a9fee5104..d998729c69d 100644 --- a/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java +++ b/firebase-common/src/main/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorage.java @@ -15,10 +15,14 @@ package com.google.firebase.heartbeatinfo; import android.content.Context; -import android.content.SharedPreferences; import android.os.Build; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; +import androidx.datastore.preferences.core.MutablePreferences; +import androidx.datastore.preferences.core.Preferences; +import androidx.datastore.preferences.core.PreferencesKeys; +import com.google.firebase.datastorage.JavaDataStorage; +import com.google.firebase.datastorage.JavaDataStorageKt; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; @@ -40,91 +44,98 @@ class HeartBeatInfoStorage { private static HeartBeatInfoStorage instance = null; - private static final String GLOBAL = "fire-global"; + private static final Preferences.Key GLOBAL = PreferencesKeys.longKey("fire-global"); private static final String PREFERENCES_NAME = "FirebaseAppHeartBeat"; private static final String HEARTBEAT_PREFERENCES_NAME = "FirebaseHeartBeat"; - private static final String HEART_BEAT_COUNT_TAG = "fire-count"; + private static final Preferences.Key HEART_BEAT_COUNT_TAG = + PreferencesKeys.longKey("fire-count"); - private static final String LAST_STORED_DATE = "last-used-date"; + private static final Preferences.Key LAST_STORED_DATE = + PreferencesKeys.stringKey("last-used-date"); // As soon as you hit the limit of heartbeats. The number of stored heartbeats is halved. private static final int HEART_BEAT_COUNT_LIMIT = 30; - private final SharedPreferences firebaseSharedPreferences; + private final JavaDataStorage firebaseDataStore; public HeartBeatInfoStorage(Context applicationContext, String persistenceKey) { - this.firebaseSharedPreferences = - applicationContext.getSharedPreferences( - HEARTBEAT_PREFERENCES_NAME + persistenceKey, Context.MODE_PRIVATE); + this.firebaseDataStore = + new JavaDataStorage(applicationContext, HEARTBEAT_PREFERENCES_NAME + persistenceKey); } @VisibleForTesting @RestrictTo(RestrictTo.Scope.TESTS) - HeartBeatInfoStorage(SharedPreferences firebaseSharedPreferences) { - this.firebaseSharedPreferences = firebaseSharedPreferences; + HeartBeatInfoStorage(JavaDataStorage javaDataStorage) { + this.firebaseDataStore = javaDataStorage; } @VisibleForTesting @RestrictTo(RestrictTo.Scope.TESTS) int getHeartBeatCount() { - return (int) this.firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); + return this.firebaseDataStore.getSync(HEART_BEAT_COUNT_TAG, 0L).intValue(); } synchronized void deleteAllHeartBeats() { - SharedPreferences.Editor editor = firebaseSharedPreferences.edit(); - int counter = 0; - for (Map.Entry entry : this.firebaseSharedPreferences.getAll().entrySet()) { - if (entry.getValue() instanceof Set) { - // All other heartbeats other than the heartbeats stored today will be deleted. - Set dates = (Set) entry.getValue(); - String today = getFormattedDate(System.currentTimeMillis()); - String key = entry.getKey(); - if (dates.contains(today)) { - Set userAgentDateSet = new HashSet<>(); - userAgentDateSet.add(today); - counter += 1; - editor.putStringSet(key, userAgentDateSet); - } else { - editor.remove(key); - } - } - } - if (counter == 0) { - editor.remove(HEART_BEAT_COUNT_TAG); - } else { - editor.putLong(HEART_BEAT_COUNT_TAG, counter); - } + firebaseDataStore.editSync( + (pref) -> { + long counter = 0; + for (Map.Entry, Object> entry : pref.asMap().entrySet()) { + if (entry.getValue() instanceof Set) { + // All other heartbeats other than the heartbeats stored today will be deleted. + Preferences.Key> key = (Preferences.Key>) entry.getKey(); + Set dates = (Set) entry.getValue(); + String today = getFormattedDate(System.currentTimeMillis()); + + if (dates.contains(today)) { + pref.set(key, Set.of(today)); + counter += 1; + } else { + pref.remove(key); + } + } + } + if (counter == 0) { + pref.remove(HEART_BEAT_COUNT_TAG); + } else { + pref.set(HEART_BEAT_COUNT_TAG, counter); + } - editor.commit(); + return null; + }); } synchronized List getAllHeartBeats() { ArrayList heartBeatResults = new ArrayList<>(); - for (Map.Entry entry : this.firebaseSharedPreferences.getAll().entrySet()) { + String today = getFormattedDate(System.currentTimeMillis()); + + for (Map.Entry, Object> entry : + this.firebaseDataStore.getAllSync().entrySet()) { if (entry.getValue() instanceof Set) { Set dates = new HashSet<>((Set) entry.getValue()); - String today = getFormattedDate(System.currentTimeMillis()); dates.remove(today); if (!dates.isEmpty()) { heartBeatResults.add( - HeartBeatResult.create(entry.getKey(), new ArrayList(dates))); + HeartBeatResult.create(entry.getKey().getName(), new ArrayList<>(dates))); } } } + updateGlobalHeartBeat(System.currentTimeMillis()); + return heartBeatResults; } - private synchronized String getStoredUserAgentString(String dateString) { - for (Map.Entry entry : firebaseSharedPreferences.getAll().entrySet()) { + private synchronized Preferences.Key> getStoredUserAgentString( + MutablePreferences preferences, String dateString) { + for (Map.Entry, Object> entry : preferences.asMap().entrySet()) { if (entry.getValue() instanceof Set) { Set dateSet = (Set) entry.getValue(); for (String date : dateSet) { if (dateString.equals(date)) { - return entry.getKey(); + return PreferencesKeys.stringSetKey(entry.getKey().getName()); } } } @@ -132,36 +143,40 @@ private synchronized String getStoredUserAgentString(String dateString) { return null; } - private synchronized void updateStoredUserAgent(String userAgent, String dateString) { - removeStoredDate(dateString); + private synchronized void updateStoredUserAgent( + MutablePreferences preferences, Preferences.Key> userAgent, String dateString) { + removeStoredDate(preferences, dateString); Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgent, new HashSet())); + new HashSet<>(JavaDataStorageKt.getOrDefault(preferences, userAgent, new HashSet<>())); userAgentDateSet.add(dateString); - firebaseSharedPreferences.edit().putStringSet(userAgent, userAgentDateSet).commit(); + preferences.set(userAgent, userAgentDateSet); } - private synchronized void removeStoredDate(String dateString) { + private synchronized void removeStoredDate(MutablePreferences preferences, String dateString) { // Find stored heartbeat and clear it. - String userAgentString = getStoredUserAgentString(dateString); - if (userAgentString == null) { + Preferences.Key> userAgent = getStoredUserAgentString(preferences, dateString); + if (userAgent == null) { return; } Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); + new HashSet<>(JavaDataStorageKt.getOrDefault(preferences, userAgent, new HashSet<>())); userAgentDateSet.remove(dateString); if (userAgentDateSet.isEmpty()) { - firebaseSharedPreferences.edit().remove(userAgentString).commit(); + preferences.remove(userAgent); } else { - firebaseSharedPreferences.edit().putStringSet(userAgentString, userAgentDateSet).commit(); + preferences.set(userAgent, userAgentDateSet); } } synchronized void postHeartBeatCleanUp() { String dateString = getFormattedDate(System.currentTimeMillis()); - firebaseSharedPreferences.edit().putString(LAST_STORED_DATE, dateString).commit(); - removeStoredDate(dateString); + + firebaseDataStore.editSync( + (pref) -> { + pref.set(LAST_STORED_DATE, dateString); + removeStoredDate(pref, dateString); + return null; + }); } private synchronized String getFormattedDate(long millis) { @@ -176,71 +191,77 @@ private synchronized String getFormattedDate(long millis) { synchronized void storeHeartBeat(long millis, String userAgentString) { String dateString = getFormattedDate(millis); - String lastDateString = firebaseSharedPreferences.getString(LAST_STORED_DATE, ""); - if (lastDateString.equals(dateString)) { - String storedUserAgentString = getStoredUserAgentString(dateString); - if (storedUserAgentString == null) { - // Heartbeat already sent for today. - return; - } - if (storedUserAgentString.equals(userAgentString)) { - // UserAgent not updated. - return; - } else { - updateStoredUserAgent(userAgentString, dateString); - return; - } - } - long heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); - if (heartBeatCount + 1 == HEART_BEAT_COUNT_LIMIT) { - cleanUpStoredHeartBeats(); - heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); - } - Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); - userAgentDateSet.add(dateString); - heartBeatCount += 1; - firebaseSharedPreferences - .edit() - .putStringSet(userAgentString, userAgentDateSet) - .putLong(HEART_BEAT_COUNT_TAG, heartBeatCount) - .putString(LAST_STORED_DATE, dateString) - .commit(); + Preferences.Key> userAgent = PreferencesKeys.stringSetKey(userAgentString); + firebaseDataStore.editSync( + (pref) -> { + String lastDateString = JavaDataStorageKt.getOrDefault(pref, LAST_STORED_DATE, ""); + if (lastDateString.equals(dateString)) { + Preferences.Key> storedUserAgent = + getStoredUserAgentString(pref, dateString); + if (storedUserAgent == null) { + // Heartbeat already sent for today. + return null; + } else if (storedUserAgent.getName().equals(userAgentString)) { + // UserAgent not updated. + return null; + } else { + updateStoredUserAgent(pref, userAgent, dateString); + return null; + } + } + long heartBeatCount = JavaDataStorageKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L); + if (heartBeatCount + 1 == HEART_BEAT_COUNT_LIMIT) { + heartBeatCount = cleanUpStoredHeartBeats(pref); + } + Set userAgentDateSet = + new HashSet<>(JavaDataStorageKt.getOrDefault(pref, userAgent, new HashSet<>())); + userAgentDateSet.add(dateString); + heartBeatCount += 1; + + pref.set(userAgent, userAgentDateSet); + pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount); + pref.set(LAST_STORED_DATE, dateString); + + return null; + }); } - private synchronized void cleanUpStoredHeartBeats() { - long heartBeatCount = firebaseSharedPreferences.getLong(HEART_BEAT_COUNT_TAG, 0); + private synchronized long cleanUpStoredHeartBeats(MutablePreferences preferences) { + long heartBeatCount = JavaDataStorageKt.getOrDefault(preferences, HEART_BEAT_COUNT_TAG, 0L); + String lowestDate = null; String userAgentString = ""; - for (Map.Entry entry : firebaseSharedPreferences.getAll().entrySet()) { + Set userAgentDateSet = new HashSet<>(); + for (Map.Entry, Object> entry : preferences.asMap().entrySet()) { if (entry.getValue() instanceof Set) { Set dateSet = (Set) entry.getValue(); for (String date : dateSet) { if (lowestDate == null || lowestDate.compareTo(date) > 0) { + userAgentDateSet = dateSet; lowestDate = date; - userAgentString = entry.getKey(); + userAgentString = entry.getKey().getName(); } } } } - Set userAgentDateSet = - new HashSet( - firebaseSharedPreferences.getStringSet(userAgentString, new HashSet())); + userAgentDateSet = new HashSet<>(userAgentDateSet); userAgentDateSet.remove(lowestDate); - firebaseSharedPreferences - .edit() - .putStringSet(userAgentString, userAgentDateSet) - .putLong(HEART_BEAT_COUNT_TAG, heartBeatCount - 1) - .commit(); + preferences.set(PreferencesKeys.stringSetKey(userAgentString), userAgentDateSet); + preferences.set(HEART_BEAT_COUNT_TAG, heartBeatCount - 1); + + return heartBeatCount - 1; } synchronized long getLastGlobalHeartBeat() { - return firebaseSharedPreferences.getLong(GLOBAL, -1); + return firebaseDataStore.getSync(GLOBAL, -1L); } synchronized void updateGlobalHeartBeat(long millis) { - firebaseSharedPreferences.edit().putLong(GLOBAL, millis).commit(); + firebaseDataStore.editSync( + (pref) -> { + pref.set(GLOBAL, millis); + return null; + }); } synchronized boolean isSameDateUtc(long base, long target) { @@ -252,15 +273,11 @@ synchronized boolean isSameDateUtc(long base, long target) { A sdk heartbeat is sent either when there is no heartbeat sent ever for the sdk or when the last heartbeat send for the sdk was later than a day before. */ - synchronized boolean shouldSendSdkHeartBeat(String heartBeatTag, long millis) { - if (firebaseSharedPreferences.contains(heartBeatTag)) { - if (!this.isSameDateUtc(firebaseSharedPreferences.getLong(heartBeatTag, -1), millis)) { - firebaseSharedPreferences.edit().putLong(heartBeatTag, millis).commit(); - return true; - } + synchronized boolean shouldSendSdkHeartBeat(Preferences.Key heartBeatTag, long millis) { + if (this.isSameDateUtc(firebaseDataStore.getSync(heartBeatTag, -1L), millis)) { return false; } else { - firebaseSharedPreferences.edit().putLong(heartBeatTag, millis).commit(); + firebaseDataStore.putSync(heartBeatTag, millis); return true; } } diff --git a/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt b/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt deleted file mode 100644 index c126b285c99..00000000000 --- a/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.ktx - -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 java.util.concurrent.Executor -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.asCoroutineDispatcher - -/** - * All fields in this object are deprecated; Use `com.google.firebase.Firebase` instead. - * - * Single access point to all firebase SDKs from Kotlin. Acts as a target for extension methods - * provided by sdks. - * - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -object Firebase - -/** - * Accessing this object for Kotlin apps has changed; see the migration guide: - * https://firebase.google.com/docs/android/kotlin-migration. - * - * Returns the default firebase app instance. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration). - */ -val Firebase.app: FirebaseApp - get() = FirebaseApp.getInstance() - -/** - * Accessing this object for Kotlin apps has changed; see the migration guide: - * https://firebase.google.com/docs/android/kotlin-migration. - * - * Returns a named firebase app instance. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration). - */ -fun Firebase.app(name: String): FirebaseApp = FirebaseApp.getInstance(name) - -/** - * Initializes and returns a FirebaseApp. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -fun Firebase.initialize(context: Context): FirebaseApp? = FirebaseApp.initializeApp(context) - -/** - * Initializes and returns a FirebaseApp. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -fun Firebase.initialize(context: Context, options: FirebaseOptions): FirebaseApp = - FirebaseApp.initializeApp(context, options) - -/** - * Initializes and returns a FirebaseApp. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -fun Firebase.initialize(context: Context, options: FirebaseOptions, name: String): FirebaseApp = - FirebaseApp.initializeApp(context, options, name) - -/** - * Accessing this object for Kotlin apps has changed; see the migration guide: - * https://firebase.google.com/docs/android/kotlin-migration. - * - * Returns options of default FirebaseApp - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase.firebase-common-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -val Firebase.options: FirebaseOptions - get() = Firebase.app.options - -/** @suppress */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -@Keep -class FirebaseCommonKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return listOf( - 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/src/test/java/com/google/firebase/DataCollectionPreNDefaultEnabledTest.java b/firebase-common/src/test/java/com/google/firebase/DataCollectionPreNDefaultEnabledTest.java index 6fa54fd6366..7ef3b3bcf29 100644 --- a/firebase-common/src/test/java/com/google/firebase/DataCollectionPreNDefaultEnabledTest.java +++ b/firebase-common/src/test/java/com/google/firebase/DataCollectionPreNDefaultEnabledTest.java @@ -27,7 +27,7 @@ import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) -@Config(sdk = 21) +@Config(sdk = Config.OLDEST_SDK) public class DataCollectionPreNDefaultEnabledTest { @Test diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java index fe564ca242e..3ffc0795b26 100644 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java @@ -25,10 +25,10 @@ import static org.mockito.Mockito.when; import android.content.Context; -import android.content.SharedPreferences; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableSet; +import com.google.firebase.datastorage.JavaDataStorage; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -107,10 +107,8 @@ public void generateHeartBeat_oneHeartBeat() throws InterruptedException, Timeou public void firstNewThenOld_synchronizedCorrectly() throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); - SharedPreferences heartBeatSharedPreferences = - context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + JavaDataStorage heartBeatDataStore = new JavaDataStorage(context, "testHeartBeat"); + HeartBeatInfoStorage heartBeatInfoStorage = new HeartBeatInfoStorage(heartBeatDataStore); DefaultHeartBeatController controller = new DefaultHeartBeatController( () -> heartBeatInfoStorage, logSources, executor, () -> publisher, context); @@ -130,10 +128,8 @@ public void firstNewThenOld_synchronizedCorrectly() public void firstOldThenNew_synchronizedCorrectly() throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); - SharedPreferences heartBeatSharedPreferences = - context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + JavaDataStorage heartBeatDataStore = new JavaDataStorage(context, "testHeartBeat"); + HeartBeatInfoStorage heartBeatInfoStorage = new HeartBeatInfoStorage(heartBeatDataStore); DefaultHeartBeatController controller = new DefaultHeartBeatController( () -> heartBeatInfoStorage, logSources, executor, () -> publisher, context); diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java index 81b191f117d..6a56ef231d1 100644 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/HeartBeatInfoStorageTest.java @@ -17,12 +17,15 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import android.content.SharedPreferences; +import androidx.datastore.preferences.core.Preferences; +import androidx.datastore.preferences.core.PreferencesKeys; import androidx.test.core.app.ApplicationProvider; import androidx.test.runner.AndroidJUnit4; +import com.google.firebase.datastorage.JavaDataStorage; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.Set; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,23 +34,31 @@ @RunWith(AndroidJUnit4.class) public class HeartBeatInfoStorageTest { - private final String testSdk = "testSdk"; - private final String GLOBAL = "fire-global"; + private final Preferences.Key testSdk = PreferencesKeys.longKey("testSdk"); + private static final Preferences.Key GLOBAL = PreferencesKeys.longKey("fire-global"); private static final int HEART_BEAT_COUNT_LIMIT = 30; - private static Context applicationContext = ApplicationProvider.getApplicationContext(); - private static SharedPreferences heartBeatSharedPreferences = - applicationContext.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); - private HeartBeatInfoStorage heartBeatInfoStorage = - new HeartBeatInfoStorage(heartBeatSharedPreferences); + private static final Context applicationContext = ApplicationProvider.getApplicationContext(); + private static final JavaDataStorage heartBeatDataStore = + new JavaDataStorage(applicationContext, "testHeartBeat"); + private final HeartBeatInfoStorage heartBeatInfoStorage = + new HeartBeatInfoStorage(heartBeatDataStore); @Before public void setUp() { - heartBeatSharedPreferences.edit().clear().apply(); + heartBeatDataStore.editSync( + (pref) -> { + pref.clear(); + return null; + }); } @After public void tearDown() { - heartBeatSharedPreferences.edit().clear().apply(); + heartBeatDataStore.editSync( + (pref) -> { + pref.clear(); + return null; + }); } @Config(sdk = 29) @@ -169,31 +180,32 @@ public void storeExcessHeartBeats_cleanUpProperly() { public void shouldSendSdkHeartBeat_answerIsYes() { long currentTime = System.currentTimeMillis(); assertThat(heartBeatInfoStorage.shouldSendSdkHeartBeat(testSdk, 1)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(testSdk, -1)).isEqualTo(1); + assertThat(heartBeatDataStore.getSync(testSdk, -1L)).isEqualTo(1); assertThat(heartBeatInfoStorage.shouldSendSdkHeartBeat(testSdk, currentTime)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(testSdk, -1)).isEqualTo(currentTime); + assertThat(heartBeatDataStore.getSync(testSdk, -1L)).isEqualTo(currentTime); } @Test public void shouldSendGlobalHeartBeat_answerIsNo() { - heartBeatSharedPreferences.edit().putLong(GLOBAL, 1).apply(); + heartBeatDataStore.putSync(GLOBAL, 1L); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(1)).isFalse(); } @Test public void currentDayHeartbeatNotSent_updatesCorrectly() { long millis = System.currentTimeMillis(); + Preferences.Key> testAgent = PreferencesKeys.stringSetKey("test-agent"); + Preferences.Key> testAgent1 = PreferencesKeys.stringSetKey("test-agent-1"); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(0); heartBeatInfoStorage.storeHeartBeat(millis, "test-agent"); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(1); assertThat(heartBeatInfoStorage.getAllHeartBeats().size()).isEqualTo(0); heartBeatInfoStorage.deleteAllHeartBeats(); assertThat(heartBeatInfoStorage.getHeartBeatCount()).isEqualTo(1); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent", new HashSet<>())).isNotEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent, new HashSet<>())).isNotEmpty(); heartBeatInfoStorage.storeHeartBeat(millis, "test-agent-1"); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent", new HashSet<>())).isEmpty(); - assertThat(heartBeatSharedPreferences.getStringSet("test-agent-1", new HashSet<>())) - .isNotEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent, new HashSet<>())).isEmpty(); + assertThat(heartBeatDataStore.getSync(testAgent1, new HashSet<>())).isNotEmpty(); } @Test @@ -222,8 +234,8 @@ public void isSameDate_returnsCorrectly() { public void shouldSendGlobalHeartBeat_answerIsYes() { long currentTime = System.currentTimeMillis(); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(1)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(GLOBAL, -1)).isEqualTo(1); + assertThat(heartBeatDataStore.getSync(GLOBAL, -1L)).isEqualTo(1); assertThat(heartBeatInfoStorage.shouldSendGlobalHeartBeat(currentTime)).isTrue(); - assertThat(heartBeatSharedPreferences.getLong(GLOBAL, -1)).isEqualTo(currentTime); + assertThat(heartBeatDataStore.getSync(GLOBAL, -1L)).isEqualTo(currentTime); } } diff --git a/firebase-common/src/test/java/com/google/firebase/ktx/Tests.kt b/firebase-common/src/test/java/com/google/firebase/ktx/Tests.kt deleted file mode 100644 index 4adc1167dbc..00000000000 --- a/firebase-common/src/test/java/com/google/firebase/ktx/Tests.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.ktx - -import androidx.test.core.app.ApplicationProvider -import com.google.android.gms.tasks.Tasks -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.platforminfo.UserAgentPublisher -import kotlinx.coroutines.tasks.await -import kotlinx.coroutines.test.runTest -import org.junit.Assert.fail -import org.junit.Test -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() - } -} - -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") - } - } -} - -@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(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#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() - } - } -} - -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. - - @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() - - 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() - } - } -} diff --git a/firebase-common/src/test/java/com/google/firebase/platforminfo/FirebasePlatformLoggingTest.java b/firebase-common/src/test/java/com/google/firebase/platforminfo/FirebasePlatformLoggingTest.java index 86172a107ce..46c301927de 100644 --- a/firebase-common/src/test/java/com/google/firebase/platforminfo/FirebasePlatformLoggingTest.java +++ b/firebase-common/src/test/java/com/google/firebase/platforminfo/FirebasePlatformLoggingTest.java @@ -87,7 +87,7 @@ public void test_auto_atHighEnoughApiLevel() { } @Test - @Config(sdk = Build.VERSION_CODES.LOLLIPOP_MR1) + @Config(sdk = Config.OLDEST_SDK) public void test_auto_atNotHighEnoughApiLevel() { ShadowPackageManager shadowPackageManager = shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); @@ -98,7 +98,7 @@ public void test_auto_atNotHighEnoughApiLevel() { app -> { UserAgentPublisher ua = app.get(UserAgentPublisher.class); - assertThat(ua.getUserAgent()).containsMatch(Pattern.compile("android-platform/($|\\s)")); + assertThat(ua.getUserAgent()).containsMatch(Pattern.compile("android-installer/($|\\s)")); }); } diff --git a/firebase-components/CHANGELOG.md b/firebase-components/CHANGELOG.md index 3db1bc4cba3..73f17b8e426 100644 --- a/firebase-components/CHANGELOG.md +++ b/firebase-components/CHANGELOG.md @@ -1,17 +1,20 @@ # Unreleased +# 19.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. # 18.0.1 -* [fixed] updated proguard rules to keep component registrar working with newer proguard versions. +- [fixed] updated proguard rules to keep component registrar working with newer proguard versions. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-components` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-components` library. The +Kotlin extensions library has no additional updates. # 17.1.2 -* [changed] Internal changes to ensure only one interface is provided for + +- [changed] Internal changes to ensure only one interface is provided for kotlinx.coroutines.CoroutineDispatcher interfaces when both firebase-common and firebase-common-ktx provide them. - diff --git a/firebase-components/firebase-components.gradle.kts b/firebase-components/firebase-components.gradle.kts index 212e8b7c488..14ff598165f 100644 --- a/firebase-components/firebase-components.gradle.kts +++ b/firebase-components/firebase-components.gradle.kts @@ -44,7 +44,7 @@ android { } dependencies { - api("com.google.firebase:firebase-annotations:16.2.0") + api(libs.firebase.annotations) implementation(libs.androidx.annotation) implementation(libs.errorprone.annotations) diff --git a/firebase-components/firebase-dynamic-module-support/CHANGELOG.md b/firebase-components/firebase-dynamic-module-support/CHANGELOG.md index 8f757dc93a9..e18540f051a 100644 --- a/firebase-components/firebase-dynamic-module-support/CHANGELOG.md +++ b/firebase-components/firebase-dynamic-module-support/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +# 16.0.0-beta04 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. # 16.0.0-beta03 -* [changed] Updated dependency of play-services-basement to its latest version (v18.1.0). + +- [changed] Updated dependency of play-services-basement to its latest version (v18.1.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 index bdce2e7c87c..1166b008e22 100644 --- 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 @@ -22,7 +22,6 @@ firebaseLibrary { releaseNotes { name.set("Dynamic feature modules support") versionName.set("dynamic-feature-modules-support") - hasKTX.set(false) } } diff --git a/firebase-components/firebase-dynamic-module-support/gradle.properties b/firebase-components/firebase-dynamic-module-support/gradle.properties index 4b7021762ba..8df0c9d32e3 100644 --- a/firebase-components/firebase-dynamic-module-support/gradle.properties +++ b/firebase-components/firebase-dynamic-module-support/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=16.0.1 -latestReleasedVersion=16.0.0-beta03 +version=16.0.0-beta05 +latestReleasedVersion=16.0.0-beta04 diff --git a/firebase-components/gradle.properties b/firebase-components/gradle.properties index 7064e6cd1ba..e23b23d6f5e 100644 --- a/firebase-components/gradle.properties +++ b/firebase-components/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=18.0.2 -latestReleasedVersion=18.0.1 +version=19.0.1 +latestReleasedVersion=19.0.0 diff --git a/firebase-config-interop/CHANGELOG.md b/firebase-config-interop/CHANGELOG.md index 225bbc02245..54976cbf39d 100644 --- a/firebase-config-interop/CHANGELOG.md +++ b/firebase-config-interop/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased -* [unchanged] Updated to keep [config] SDK versions aligned. + +- [unchanged] Updated to keep [config] SDK versions aligned. # 16.0.0 -* [feature] Initial release. + +- [feature] Initial release. diff --git a/firebase-config/CHANGELOG.md b/firebase-config/CHANGELOG.md index fc00b486a87..fca96b9131f 100644 --- a/firebase-config/CHANGELOG.md +++ b/firebase-config/CHANGELOG.md @@ -1,476 +1,537 @@ # Unreleased +# 23.0.1 + +- [changed] Bumped internal dependencies. + +# 23.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. +- [changed] This update introduces improvements to how the SDK handles real-time requests when a + Firebase project has exceeded its available quota for real-time services. Released in anticipation + of future quota enforcement, this change is designed to fetch the latest template even when the + quota is exhausted. +- [removed] **Breaking Change**: Stopped releasing the deprecated Kotlin extensions (KTX) module and + removed it from the Firebase Android BoM. Instead, use the KTX APIs from the main module. For + details, see the + [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration). # 22.1.2 -* [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting - `HttpURLConnection` only on API levels 26 and higher. GitHub Issue [#6934] +- [fixed] Fixed `NetworkOnMainThreadException` on Android versions below 8 by disconnecting + `HttpURLConnection` only on API levels 26 and higher. GitHub Issue [#6934] ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 22.1.1 -* [fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain -open in the background. +- [fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain + open in the background. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 22.1.0 -* [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config. +- [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API + for setting custom signals and use them to build custom targeting conditions in Remote Config. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 22.0.1 -* [changed] Updated protobuf dependency to `3.25.5` to fix - [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). +- [changed] Updated protobuf dependency to `3.25.5` to fix + [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 22.0.0 -* [changed] Bump internal dependencies +- [changed] Bump internal dependencies ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.6.3 -* [fixed] Fixed a bug that could cause a crash if the app was backgrounded - while it was listening for real-time Remote Config updates. For more information, see #5751 + +- [fixed] Fixed a bug that could cause a crash if the app was backgrounded while it was listening + for real-time Remote Config updates. For more information, see #5751 # 21.6.2 -* [fixed] Fixed an issue that could cause [remote_config] personalizations to be logged early in + +- [fixed] Fixed an issue that could cause [remote_config] personalizations to be logged early in specific cases. -* [fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain +- [fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain open in the background. - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.6.1 -* [changed] Bump internal dependencies. +- [changed] Bump internal dependencies. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.6.0 -* [changed] Added support for other Firebase products to integrate with [remote_config]. + +- [changed] Added support for other Firebase products to integrate with [remote_config]. # 21.5.0 -* [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-config-ktx` - to `com.google.firebase:firebase-config` under the `com.google.firebase.remoteconfig` package. - For details, see the + +- [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-config-ktx` to + `com.google.firebase:firebase-config` under the `com.google.firebase.remoteconfig` package. For + details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) -* [deprecated] All the APIs from `com.google.firebase:firebase-config-ktx` have been added to - `com.google.firebase:firebase-config` under the `com.google.firebase.remoteconfig` package, - and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-config-ktx` are - now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the +- [deprecated] All the APIs from `com.google.firebase:firebase-config-ktx` have been added to + `com.google.firebase:firebase-config` under the `com.google.firebase.remoteconfig` package, and + all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-config-ktx` are now + deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.4.1 -* [changed] Internal improvements to support Remote Config real-time updates. +- [changed] Internal improvements to support Remote Config real-time updates. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.4.0 -* [unchanged] Updated to accommodate the release of the updated - [remote_config] Kotlin extensions library. +- [unchanged] Updated to accommodate the release of the updated [remote_config] Kotlin extensions + library. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has the following -additional updates. -* [feature] Added the - [`FirebaseRemoteConfig.configUpdates`](/docs/reference/kotlin/com/google/firebase/remoteconfig/ktx/package-summary#(com.google.firebase.remoteconfig.FirebaseRemoteConfig).configUpdates()) +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has the following additional updates. + +- [feature] Added the + [`FirebaseRemoteConfig.configUpdates`]() Kotlin Flow to listen for real-time config updates. # 21.3.0 -* [feature] Added support for real-time config updates. To learn more, see - [Get started with [firebase_remote_config]](/docs/remote-config/get-started?platform=android#add-real-time-listener). +- [feature] Added support for real-time config updates. To learn more, see + [Get started with [firebase_remote_config]](/docs/remote-config/get-started?platform=android#add-real-time-listener). ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.2.1 -* [changed] Migrated [remote_config] to use standard Firebase executors. +- [changed] Migrated [remote_config] to use standard Firebase executors. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.2.0 -* [unchanged] Updated to accommodate the release of the updated - [remote_config] Kotlin extensions library. +- [unchanged] Updated to accommodate the release of the updated [remote_config] Kotlin extensions + library. ## Kotlin -The Kotlin extensions library transitively includes the updated - `firebase-config` library. The Kotlin extensions library has the following - additional updates: -* [feature] Firebase now supports Kotlin coroutines. - With this release, we added - [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: .external} - to `firebase-config-ktx` as a transitive dependency, which exposes the +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has the following additional updates: + +- [feature] Firebase now supports Kotlin coroutines. With this release, we added + [`kotlinx-coroutines-play-services`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-play-services/){: + .external} to `firebase-config-ktx` as a transitive dependency, which exposes the `Task.await()` suspend function to convert a - [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin - coroutine. + [`Task`](https://developers.google.com/android/guides/tasks) into a Kotlin coroutine. # 21.1.2 -* [changed] Updated dependency of `play-services-basement` to its latest - version (v18.1.0). +- [changed] Updated dependency of `play-services-basement` to its latest version (v18.1.0). ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.1.1 -* [fixed] Fixed a bug that caused HTTP errors in some locales. For more - information, see - GitHub Issue #3757 +- [fixed] Fixed a bug that caused HTTP errors in some locales. For more information, see + GitHub Issue #3757 ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.1.0 + -* [changed] Added first-open time to [remote_config] server requests. - +- [changed] Added first-open time to [remote_config] server requests. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.0.2 -* [changed] Updated dependencies of `play-services-basement`, - `play-services-base`, and `play-services-tasks` to their latest versions - (v18.0.0, v18.0.1, and v18.0.1, respectively). For more information, see the - [note](#basement18-0-0_base18-0-1_tasks18-0-1) at the top of this release - entry. +- [changed] Updated dependencies of `play-services-basement`, `play-services-base`, and + `play-services-tasks` to their latest versions (v18.0.0, v18.0.1, and v18.0.1, respectively). For + more information, see the [note](#basement18-0-0_base18-0-1_tasks18-0-1) at the top of this + release entry. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.0.1 -* [fixed] Fixed a bug in the initialization of [remote_config] with a - non-primary Firebase app. +- [fixed] Fixed a bug in the initialization of [remote_config] with a non-primary Firebase app. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 21.0.0 -* [changed] Internal infrastructure improvements. -* [changed] Internal changes to support dynamic feature modules. +- [changed] Internal infrastructure improvements. +- [changed] Internal changes to support dynamic feature modules. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 20.0.4 -* [changed] Improved condition targeting signals. +- [changed] Improved condition targeting signals. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 20.0.3 -* [changed] Standardize support for other Firebase products that integrate - with [remote_config]. +- [changed] Standardize support for other Firebase products that integrate with [remote_config]. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 20.0.2 -* [fixed] Fixed an issue that was causing [remote_config] to return the - static default value even if a remote value was defined. (#2186) +- [fixed] Fixed an issue that was causing [remote_config] to return the static default value even if + a remote value was defined. (#2186) ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 20.0.1 -* [changed] Added support for other Firebase products to integrate with - [remote_config]. +- [changed] Added support for other Firebase products to integrate with [remote_config]. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 20.0.0 -* [removed] Removed the protocol buffer dependency. Also, removed support for - configs saved on device using the legacy protocol buffer format (the SDK - stopped using this legacy format starting with [remote_config] v16.3.0). -* [removed] Removed the deprecated synchronous method - `FirebaseRemoteConfig.activateFetched()`. Use the asynchronous - [`FirebaseRemoteConfig.activate()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#activate()) + +- [removed] Removed the protocol buffer dependency. Also, removed support for configs saved on + device using the legacy protocol buffer format (the SDK stopped using this legacy format starting + with [remote_config] v16.3.0). +- [removed] Removed the deprecated synchronous method `FirebaseRemoteConfig.activateFetched()`. Use + the asynchronous + [`FirebaseRemoteConfig.activate()`]() instead. -* [removed] Removed the deprecated synchronous methods - `FirebaseRemoteConfig.setDefaults(int)` and +- [removed] Removed the deprecated synchronous methods `FirebaseRemoteConfig.setDefaults(int)` and `FirebaseRemoteConfig.setDefaults(Map)`. Use the asynchronous - [`FirebaseRemoteConfig.setDefaultsAsync(int)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(int)) - and [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(Map)) + [`FirebaseRemoteConfig.setDefaultsAsync(int)`]() + and + [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](>) instead. -* [removed] Removed the deprecated synchronous method - `FirebaseRemoteConfig.setConfigSettings(FirebaseRemoteConfigSettings)`. - Use the asynchronous - [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setConfigSettingsAsync(FirebaseRemoteConfigSettings)) +- [removed] Removed the deprecated synchronous method + `FirebaseRemoteConfig.setConfigSettings(FirebaseRemoteConfigSettings)`. Use the asynchronous + [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`]() instead. -* [removed] Removed the deprecated method - `FirebaseRemoteConfig.getByteArray(String)`. Use - [`FirebaseRemoteConfig.getString(String)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#getString(String)) +- [removed] Removed the deprecated method `FirebaseRemoteConfig.getByteArray(String)`. Use + [`FirebaseRemoteConfig.getString(String)`]() instead. -* [removed] Removed the deprecated methods - `FirebaseRemoteConfigSettings.isDeveloperModeEnabled()` and - `FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled(boolean)`. Use - [`FirebaseRemoteConfigSettings#getMinimumFetchIntervalInSeconds()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings#getMinimumFetchIntervalInSeconds()) - and [`FirebaseRemoteConfigSettings.Builder#setMinimumFetchIntervalInSeconds(long)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder#setMinimumFetchIntervalInSeconds(long)) +- [removed] Removed the deprecated methods `FirebaseRemoteConfigSettings.isDeveloperModeEnabled()` + and `FirebaseRemoteConfigSettings.Builder.setDeveloperModeEnabled(boolean)`. Use + [`FirebaseRemoteConfigSettings#getMinimumFetchIntervalInSeconds()`]() + and + [`FirebaseRemoteConfigSettings.Builder#setMinimumFetchIntervalInSeconds(long)`]() instead. - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.2.0 -* [changed] Migrated to use the [firebase_installations] service _directly_ - instead of using an indirect dependency via the Firebase Instance ID SDK. + +- [changed] Migrated to use the [firebase_installations] service _directly_ instead of using an + indirect dependency via the Firebase Instance ID SDK. {% include "docs/reference/android/client/_includes/_iid-indirect-dependency-solutions.html" %} -* [changed] Updated the protocol buffer dependency to the newer - `protobuf-javalite` artifact. The new artifact is incompatible with the old - one, so this library needed to be upgraded to avoid conflicts. No developer - action is necessary. +- [changed] Updated the protocol buffer dependency to the newer `protobuf-javalite` artifact. The + new artifact is incompatible with the old one, so this library needed to be upgraded to avoid + conflicts. No developer action is necessary. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.1.4 -* [changed] Updated dependency on the Firebase Instance ID library to v20.1.5, - which is a step towards a direct dependency on the Firebase installations - service in a future release. +- [changed] Updated dependency on the Firebase Instance ID library to v20.1.5, which is a step + towards a direct dependency on the Firebase installations service in a future release. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.1.3 -* [fixed] Fixed an issue where [`FirebaseRemoteConfig.fetch()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#fetch()) -would sometimes report a misformatted language tag. +- [fixed] Fixed an issue where + [`FirebaseRemoteConfig.fetch()`]() + would sometimes report a misformatted language tag. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.1.2 -* [fixed] Resolved known issue where + +- [fixed] Resolved known issue where [`FirebaseRemoteConfigSettings.Builder.setFetchTimeoutInSeconds()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder) was not always honored. - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.1.1 -* [changed] Updated [`FirebaseRemoteConfig.fetch()`](docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig.html#fetch()) -implementation to use [`FirebaseInstanceId.getInstanceId()`](/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#getInstanceId()) -in favor of the deprecated [`FirebaseInstanceId.getToken()`](/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#getToken()). +- [changed] Updated + [`FirebaseRemoteConfig.fetch()`]() + implementation to use + [`FirebaseInstanceId.getInstanceId()`]() + in favor of the deprecated + [`FirebaseInstanceId.getToken()`](). ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.1.0 -* [changed] Added getters to the fields of the + +- [changed] Added getters to the fields of the [`FirebaseRemoteConfigSettings.Builder`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder) object to provide better Kotlin patterns. - ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.0.4 -* [fixed] Resolved - [known issue](//github.com/firebase/firebase-android-sdk/issues/973) where - network calls may fail on devices using API 19 and earlier. +- [fixed] Resolved [known issue](//github.com/firebase/firebase-android-sdk/issues/973) where + network calls may fail on devices using API 19 and earlier. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.0.3 -* [fixed] Resolved - [known issue](https://github.com/firebase/firebase-android-sdk/issues/787) - where the [firebase_remote_config] SDK threw an error when Android - [StrictMode](https://developer.android.com/reference/android/os/StrictMode) - was turned on. -* [fixed] Resolved issue where setting Byte Arrays via - [`FirebaseRemoteConfig.setDefaultsAsync(int)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(int)), - [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(Map)) - and their synchronous counterparts would cause `getByteArray` to return an - object reference instead of the Byte Array. Byte Arrays set via the - Firebase console were unaffected by this bug. +- [fixed] Resolved [known issue](https://github.com/firebase/firebase-android-sdk/issues/787) where + the [firebase_remote_config] SDK threw an error when Android + [StrictMode](https://developer.android.com/reference/android/os/StrictMode) was turned on. +- [fixed] Resolved issue where setting Byte Arrays via + [`FirebaseRemoteConfig.setDefaultsAsync(int)`](), + [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](>) + and their synchronous counterparts would cause `getByteArray` to return an object reference + instead of the Byte Array. Byte Arrays set via the Firebase console were unaffected by this bug. ## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-config` library. The Kotlin extensions library has no additional -updates. + +The Kotlin extensions library transitively includes the updated `firebase-config` library. The +Kotlin extensions library has no additional updates. # 19.0.2 -* [unchanged] Updated to accommodate the release of the [remote_config] - Kotlin extensions library. +- [unchanged] Updated to accommodate the release of the [remote_config] Kotlin extensions library. ## Kotlin -* [feature] The beta release of a [remote_config] Android library with - Kotlin extensions is now available. The Kotlin extensions library transitively - includes the base `firebase-config` library. To learn more, visit the + +- [feature] The beta release of a [remote_config] Android library with Kotlin extensions is now + available. The Kotlin extensions library transitively includes the base `firebase-config` library. + To learn more, visit the [[remote_config] KTX documentation](/docs/reference/kotlin/com/google/firebase/remoteconfig/ktx/package-summary). # 19.0.1 -* [fixed] Resolved known issue where certain unicode characters were not - encoded correctly. The issue was introduced in v19.0.0. + +- [fixed] Resolved known issue where certain unicode characters were not encoded correctly. The + issue was introduced in v19.0.0. # 19.0.0 -* [changed] Versioned to add nullability annotations to improve the Kotlin - developer experience. No other changes. + +- [changed] Versioned to add nullability annotations to improve the Kotlin developer experience. No + other changes. # 17.0.0 -* [feature] Added an asynchronous way to set config settings: [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setConfigSettingsAsync(FirebaseRemoteConfigSettings)). -* [feature] Added [`FirebaseRemoteConfigServerException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigServerException) and [`FirebaseRemoteConfigClientException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientException) to provide more nuanced error reporting. -* [changed] Updated all "cache expiration" references to "minimum fetch interval" and "cache" references to "local storage". -* [deprecated] Deprecated developer mode. Use [`FirebaseRemoteConfigSettings.Builder.setMinimumFetchIntervalInSeconds(0L)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder#setMinimumFetchIntervalInSeconds(long)) instead. -* [deprecated] Deprecated the synchronous [`FirebaseRemoteConfig.setConfigSettings(FirebaseRemoteConfigSettings)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setConfigSettings(FirebaseRemoteConfigSettings)). Use the asynchronous [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setConfigSettingsAsync(FirebaseRemoteConfigSettings)) instead. -* [deprecated] Deprecated [`FirebaseRemoteConfigFetchException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchException). Use the more granular [`FirebaseRemoteConfigServerException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigServerException) and [`FirebaseRemoteConfigClientException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientException) instead. -* [removed] Removed all namespace methods. -* [removed] Removed all default constructors for Exception classes. -* [changed] Updated minSdkVersion to API level 16. + +- [feature] Added an asynchronous way to set config settings: + [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`](). +- [feature] Added + [`FirebaseRemoteConfigServerException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigServerException) + and + [`FirebaseRemoteConfigClientException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientException) + to provide more nuanced error reporting. +- [changed] Updated all "cache expiration" references to "minimum fetch interval" and "cache" + references to "local storage". +- [deprecated] Deprecated developer mode. Use + [`FirebaseRemoteConfigSettings.Builder.setMinimumFetchIntervalInSeconds(0L)`]() + instead. +- [deprecated] Deprecated the synchronous + [`FirebaseRemoteConfig.setConfigSettings(FirebaseRemoteConfigSettings)`](). + Use the asynchronous + [`FirebaseRemoteConfig.setConfigSettingsAsync(FirebaseRemoteConfigSettings)`]() + instead. +- [deprecated] Deprecated + [`FirebaseRemoteConfigFetchException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigFetchException). + Use the more granular + [`FirebaseRemoteConfigServerException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigServerException) + and + [`FirebaseRemoteConfigClientException`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientException) + instead. +- [removed] Removed all namespace methods. +- [removed] Removed all default constructors for Exception classes. +- [changed] Updated minSdkVersion to API level 16. # 16.5.0 -* [feature] Enabled multi-App support. Use [`FirebaseRemoteConfig.getInstance(FirebaseApp)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#getInstance(FirebaseApp)) to retrieve a singleton instance of [`FirebaseRemoteConfig`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig) for the given [`FirebaseApp`](/docs/reference/android/com/google/firebase/FirebaseApp). -* [feature] Added a method that fetches configs and activates them: [`FirebaseRemoteConfig.fetchAndActivate()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#fetchAndActivate()). -* [feature] Network connection timeout for fetch requests is now customizable. To set the network timeout, use [`FirebaseRemoteConfigSettings.Builder.setFetchTimeoutInSeconds(long)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder#setFetchTimeoutInSeconds(long)). -* [feature] The default minimum fetch interval is now customizable. To set the default minimum fetch interval, use [`FirebaseRemoteConfigSettings.Builder.setMinimumFetchIntervalInSeconds(long)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfigSettings.Builder#setMinimumFetchIntervalInSeconds(long)). -* [feature] Added a way to get all activated configs as a Java `Map`: [`FirebaseRemoteConfig.getAll()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#getAll()). -* [feature] Added the ability to reset a Firebase Remote Config instance: [`FirebaseRemoteConfig.reset()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#reset()). -* [feature] Added a way to determine if the Firebase Remote Config instance has finished initializing. To get a task that will complete when the Firebase Remote Config instance is finished initializing, use [`FirebaseRemoteConfig.ensureInitialized()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#ensureInitialized()). -* [feature] Added an asynchronous way to activate configs: [`FirebaseRemoteConfig.activate()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#activate()). -* [feature] Added an asynchronous way to set defaults: [`FirebaseRemoteConfig.setDefaultsAsync(int)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(int)) and [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(Map)). -* [deprecated] Deprecated the synchronous [`FirebaseRemoteConfig.activateFetched()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#activateFetched()). Use the asynchronous [`FirebaseRemoteConfig.activate()`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#activate()) instead. -* [deprecated] Deprecated the synchronous [`FirebaseRemoteConfig.setDefaults(int)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaults(int)) and [`FirebaseRemoteConfig.setDefaults(Map)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefalts(Map)). Use the asynchronous [`FirebaseRemoteConfig.setDefaultsAsync(int)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(int)) and [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#setDefaultsAsync(Map)) instead. -* [deprecated] Deprecated [`FirebaseRemoteConfig.getByteArray(String)`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#getByteArray(String)). -* [deprecated] Deprecated all methods with a namespace parameter. + +- [feature] Enabled multi-App support. Use + [`FirebaseRemoteConfig.getInstance(FirebaseApp)`]() + to retrieve a singleton instance of + [`FirebaseRemoteConfig`](/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig) + for the given [`FirebaseApp`](/docs/reference/android/com/google/firebase/FirebaseApp). +- [feature] Added a method that fetches configs and activates them: + [`FirebaseRemoteConfig.fetchAndActivate()`](). +- [feature] Network connection timeout for fetch requests is now customizable. To set the network + timeout, use + [`FirebaseRemoteConfigSettings.Builder.setFetchTimeoutInSeconds(long)`](). +- [feature] The default minimum fetch interval is now customizable. To set the default minimum fetch + interval, use + [`FirebaseRemoteConfigSettings.Builder.setMinimumFetchIntervalInSeconds(long)`](). +- [feature] Added a way to get all activated configs as a Java `Map`: + [`FirebaseRemoteConfig.getAll()`](). +- [feature] Added the ability to reset a Firebase Remote Config instance: + [`FirebaseRemoteConfig.reset()`](). +- [feature] Added a way to determine if the Firebase Remote Config instance has finished + initializing. To get a task that will complete when the Firebase Remote Config instance is + finished initializing, use + [`FirebaseRemoteConfig.ensureInitialized()`](). +- [feature] Added an asynchronous way to activate configs: + [`FirebaseRemoteConfig.activate()`](). +- [feature] Added an asynchronous way to set defaults: + [`FirebaseRemoteConfig.setDefaultsAsync(int)`]() + and + [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](>). +- [deprecated] Deprecated the synchronous + [`FirebaseRemoteConfig.activateFetched()`](). + Use the asynchronous + [`FirebaseRemoteConfig.activate()`]() + instead. +- [deprecated] Deprecated the synchronous + [`FirebaseRemoteConfig.setDefaults(int)`]() + and + [`FirebaseRemoteConfig.setDefaults(Map)`](>). + Use the asynchronous + [`FirebaseRemoteConfig.setDefaultsAsync(int)`]() + and + [`FirebaseRemoteConfig.setDefaultsAsync(Map)`](>) + instead. +- [deprecated] Deprecated + [`FirebaseRemoteConfig.getByteArray(String)`](). +- [deprecated] Deprecated all methods with a namespace parameter. # 16.4.1 -* [changed] The SDK now enforces Android API Key restrictions. -* [fixed] Resolved known issue where the local cache was not honored even if - it had not expired. The issue was introduced in version 16.3.0. + +- [changed] The SDK now enforces Android API Key restrictions. +- [fixed] Resolved known issue where the local cache was not honored even if it had not expired. The + issue was introduced in version 16.3.0. # 16.4.0 -* [changed] Internal changes to ensure functionality alignment with other SDK releases. + +- [changed] Internal changes to ensure functionality alignment with other SDK releases. # 16.3.0 -* [changed] The [firebase_remote_config] SDK requires the - [firebase_remote_config] REST API. For Firebase projects created before - March 7, 2018, you must manually enable the REST API. For more information, - see our + +- [changed] The [firebase_remote_config] SDK requires the [firebase_remote_config] REST API. For + Firebase projects created before March 7, 2018, you must manually enable the REST API. For more + information, see our [[remote_config] REST API user guide](https://firebase.google.com/docs/remote-config/use-config-rest#before_you_begin_enable_the_rest_api). -* [changed] Refactored the implementation of [remote_config] to improve SDK - stability and speed, and to remove the Google Play Services dependency. -* [changed] Improved error logs and exception messages. -* [changed] Updated the Android documentation to reflect that - [remote_config] uses `Locale` to retrieve location information, similar to - iOS's use of `countryCode`. +- [changed] Refactored the implementation of [remote_config] to improve SDK stability and speed, and + to remove the Google Play Services dependency. +- [changed] Improved error logs and exception messages. +- [changed] Updated the Android documentation to reflect that [remote_config] uses `Locale` to + retrieve location information, similar to iOS's use of `countryCode`. # 16.1.3 -* [fixed] Fixed an issue where [remote_config] experiments were not - collecting results. + +- [fixed] Fixed an issue where [remote_config] experiments were not collecting results. # 16.1.0 -* [fixed] Bug fixes and internal improvements to support Firebase Performance Monitoring features. +- [fixed] Bug fixes and internal improvements to support Firebase Performance Monitoring features. diff --git a/firebase-config/api.txt b/firebase-config/api.txt index 77efe522e60..05a10cad518 100644 --- a/firebase-config/api.txt +++ b/firebase-config/api.txt @@ -143,15 +143,3 @@ package com.google.firebase.remoteconfig { } -package com.google.firebase.remoteconfig.ktx { - - public final class RemoteConfigKt { - method @Deprecated public static operator com.google.firebase.remoteconfig.FirebaseRemoteConfigValue get(com.google.firebase.remoteconfig.FirebaseRemoteConfig, String key); - method @Deprecated public static kotlinx.coroutines.flow.Flow getConfigUpdates(com.google.firebase.remoteconfig.FirebaseRemoteConfig); - method @Deprecated public static com.google.firebase.remoteconfig.FirebaseRemoteConfig getRemoteConfig(com.google.firebase.ktx.Firebase); - method @Deprecated public static com.google.firebase.remoteconfig.FirebaseRemoteConfig remoteConfig(com.google.firebase.ktx.Firebase, com.google.firebase.FirebaseApp app); - method public static com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings remoteConfigSettings(kotlin.jvm.functions.Function1 init); - } - -} - diff --git a/firebase-config/bandwagoner/bandwagoner.gradle b/firebase-config/bandwagoner/bandwagoner.gradle index 12c0ac1d508..fed8ad1427a 100644 --- a/firebase-config/bandwagoner/bandwagoner.gradle +++ b/firebase-config/bandwagoner/bandwagoner.gradle @@ -14,6 +14,8 @@ * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + apply plugin: 'com.android.application' apply plugin: com.google.firebase.gradle.plugins.ci.device.FirebaseTestLabPlugin apply plugin: 'org.jetbrains.kotlin.android' @@ -58,11 +60,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = '1.8' - } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + firebaseTestLab { device 'model=panther,version=33,locale=en,orientation=portrait' } @@ -75,12 +76,9 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' } - implementation(project(":firebase-config:ktx")) { - exclude group: 'com.google.firebase', module: 'firebase-common' - exclude group: 'com.google.firebase', module: 'firebase-components' - } implementation(project(":firebase-installations")) { exclude group: 'com.google.firebase', module: 'firebase-common' + exclude group: 'com.google.firebase', module: 'firebase-common-ktx' exclude group: 'com.google.firebase', module: 'firebase-components' } implementation libs.androidx.annotation @@ -95,9 +93,8 @@ dependencies { // "implementation" dependencies. The alternative would be to make common an "api" dep of remote-config. // Released artifacts don't need these dependencies since they don't use `project` to refer // to Remote Config. - implementation("com.google.firebase:firebase-common:21.0.0") - implementation("com.google.firebase:firebase-common-ktx:21.0.0") - implementation("com.google.firebase:firebase-components:18.0.0") + implementation("com.google.firebase:firebase-common:22.0.0") + implementation(libs.firebase.components) implementation("com.google.firebase:firebase-installations-interop:17.1.1") { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' diff --git a/firebase-config/bandwagoner/src/main/java/com/googletest/firebase/remoteconfig/bandwagoner/RealtimeKtListener.kt b/firebase-config/bandwagoner/src/main/java/com/googletest/firebase/remoteconfig/bandwagoner/RealtimeKtListener.kt index 5e77119b443..63b42954c4c 100644 --- a/firebase-config/bandwagoner/src/main/java/com/googletest/firebase/remoteconfig/bandwagoner/RealtimeKtListener.kt +++ b/firebase-config/bandwagoner/src/main/java/com/googletest/firebase/remoteconfig/bandwagoner/RealtimeKtListener.kt @@ -17,9 +17,9 @@ package com.googletest.firebase.remoteconfig.bandwagoner import android.util.Log -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ktx.configUpdates -import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.Firebase +import com.google.firebase.remoteconfig.configUpdates +import com.google.firebase.remoteconfig.remoteConfig import java.util.concurrent.CompletableFuture import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.catch diff --git a/firebase-config/firebase-config.gradle.kts b/firebase-config/firebase-config.gradle.kts index a8024d79c77..5683ee4d057 100644 --- a/firebase-config/firebase-config.gradle.kts +++ b/firebase-config/firebase-config.gradle.kts @@ -14,6 +14,8 @@ * limitations under the License. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("firebase-library") id("kotlin-android") @@ -37,7 +39,7 @@ android { compileSdk = targetSdkVersion defaultConfig { - minSdk = 21 + minSdk = rootProject.extra["minSdkVersion"] as Int multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -49,8 +51,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { jvmTarget = "1.8" } - testOptions { targetSdk = targetSdkVersion unitTests { isIncludeAndroidResources = true } @@ -58,10 +58,12 @@ android { lint { targetSdk = targetSdkVersion } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { // Firebase api("com.google.firebase:firebase-config-interop:16.0.1") - api("com.google.firebase:firebase-annotations:16.2.0") + api(libs.firebase.annotations) api("com.google.firebase:firebase-installations-interop:17.1.0") api("com.google.firebase:firebase-abt:21.1.1") { exclude(group = "com.google.firebase", module = "firebase-common") @@ -71,10 +73,11 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-common") exclude(group = "com.google.firebase", module = "firebase-components") } - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-common-ktx:21.0.0") - api("com.google.firebase:firebase-components:18.0.0") - api("com.google.firebase:firebase-installations:17.2.0") + api(libs.firebase.common) + api(libs.firebase.components) + api("com.google.firebase:firebase-installations:18.0.0") { + exclude(group = "com.google.firebase", module = "firebase-common-ktx") + } // Kotlin & Android implementation(libs.kotlin.stdlib) @@ -85,6 +88,7 @@ dependencies { annotationProcessor(libs.autovalue) javadocClasspath(libs.autovalue.annotations) compileOnly(libs.autovalue.annotations) + compileOnly(libs.errorprone.annotations) compileOnly(libs.findbugs.jsr305) // Testing diff --git a/firebase-config/gradle.properties b/firebase-config/gradle.properties index 02671b7fc28..88599534856 100644 --- a/firebase-config/gradle.properties +++ b/firebase-config/gradle.properties @@ -14,7 +14,7 @@ # limitations under the License. # -version=22.1.3 -latestReleasedVersion=22.1.2 +version=23.0.2 +latestReleasedVersion=23.0.1 android.enableUnitTestBinaryResources=true diff --git a/firebase-config/ktx/api.txt b/firebase-config/ktx/api.txt deleted file mode 100644 index da4f6cc18fe..00000000000 --- a/firebase-config/ktx/api.txt +++ /dev/null @@ -1 +0,0 @@ -// Signature format: 3.0 diff --git a/firebase-config/ktx/gradle.properties b/firebase-config/ktx/gradle.properties deleted file mode 100644 index 016fa887bc0..00000000000 --- a/firebase-config/ktx/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -android.enableUnitTestBinaryResources=true - diff --git a/firebase-config/ktx/ktx.gradle b/firebase-config/ktx/ktx.gradle deleted file mode 100644 index 64da37d23fe..00000000000 --- a/firebase-config/ktx/ktx.gradle +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -plugins { - id 'firebase-library' - id("kotlin-android") -} - -firebaseLibrary { - libraryGroup = "config" - publishJavadoc = false - releaseNotes { - enabled.set(false) - } -} - -android { - namespace "com.google.firebase.remoteconfig.ktx" - compileSdkVersion project.compileSdkVersion - defaultConfig { - minSdkVersion project.minSdkVersion - 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' - } - } - kotlinOptions { - jvmTarget = '1.8' - } - testOptions.unitTests.includeAndroidResources = true -} - -dependencies { - api(project(":firebase-config")) - api("com.google.firebase:firebase-common:21.0.0") - api("com.google.firebase:firebase-common-ktx:21.0.0") - api("com.google.firebase:firebase-installations:17.2.0") - - implementation('com.google.firebase:firebase-abt:21.1.1') { - exclude group: 'com.google.firebase', module: 'firebase-common' - exclude group: 'com.google.firebase', module: 'firebase-components' - } - implementation("com.google.firebase:firebase-components:18.0.0") - implementation 'com.google.firebase:firebase-installations-interop:17.1.0' - - testImplementation libs.androidx.test.core - testImplementation libs.truth - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation libs.robolectric -} diff --git a/firebase-config/ktx/src/main/AndroidManifest.xml b/firebase-config/ktx/src/main/AndroidManifest.xml deleted file mode 100644 index 9df18d6857b..00000000000 --- a/firebase-config/ktx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/Logging.kt b/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/Logging.kt deleted file mode 100644 index b13c960807c..00000000000 --- a/firebase-config/ktx/src/main/kotlin/com/google/firebase/remoteconfig/ktx/Logging.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2023 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.remoteconfig.ktx - -import androidx.annotation.Keep -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.platforminfo.LibraryVersionComponent -import com.google.firebase.remoteconfig.BuildConfig - -internal const val LIBRARY_NAME: String = "fire-cfg-ktx" - -/** @suppress */ -@Keep -class FirebaseConfigLegacyRegistrar : ComponentRegistrar { - override fun getComponents(): List> { - return 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 deleted file mode 100644 index 4c153bff1e3..00000000000 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/TestConstructorUtil.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.remoteconfig - -import android.content.Context -import com.google.firebase.FirebaseApp -import com.google.firebase.abt.FirebaseABTesting -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.remoteconfig.internal.ConfigCacheClient -import com.google.firebase.remoteconfig.internal.ConfigFetchHandler -import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler -import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler -import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient -import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler -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, - frcSharedPrefs: ConfigSharedPrefsClient, - realtimeHandler: ConfigRealtimeHandler, - rolloutsStateSubscriptionsHandler: RolloutsStateSubscriptionsHandler -): FirebaseRemoteConfig { - return FirebaseRemoteConfig( - context, - firebaseApp, - firebaseInstallations, - firebaseAbt, - executor, - fetchedConfigsCache, - activatedConfigsCache, - defaultConfigsCache, - fetchHandler, - getHandler, - frcSharedPrefs, - realtimeHandler, - rolloutsStateSubscriptionsHandler - ) -} 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 deleted file mode 100644 index d3e7d10e725..00000000000 --- a/firebase-config/ktx/src/test/kotlin/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.remoteconfig.ktx - -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.google.common.util.concurrent.MoreExecutors -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app -import com.google.firebase.ktx.initialize -import com.google.firebase.platforminfo.UserAgentPublisher -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue -import com.google.firebase.remoteconfig.createRemoteConfig -import com.google.firebase.remoteconfig.internal.ConfigCacheClient -import com.google.firebase.remoteconfig.internal.ConfigFetchHandler -import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler -import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler -import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient -import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.RobolectricTestRunner - -const val APP_ID = "1:14368190084:android:09cb977358c6f241" -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") -} - -class StringRemoteConfigValue(val value: String) : DefaultFirebaseRemoteConfigValue() { - 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() - } -} - -@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, - frcSharedPrefs = mock(ConfigSharedPrefsClient::class.java), - realtimeHandler = mock(ConfigRealtimeHandler::class.java), - rolloutsStateSubscriptionsHandler = mock(RolloutsStateSubscriptionsHandler::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) - } -} diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java index a93b1dc5784..05c46679c03 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java @@ -19,6 +19,8 @@ import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.VisibleForTesting; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.remoteconfig.ConfigUpdate; @@ -31,9 +33,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.util.Date; import java.util.Random; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.json.JSONException; import org.json.JSONObject; @@ -43,6 +47,7 @@ public class ConfigAutoFetch { private static final int MAXIMUM_FETCH_ATTEMPTS = 3; private static final String TEMPLATE_VERSION_KEY = "latestTemplateVersionNumber"; private static final String REALTIME_DISABLED_KEY = "featureDisabled"; + private static final String REALTIME_RETRY_INTERVAL = "retryIntervalSeconds"; @GuardedBy("this") private final Set eventListeners; @@ -54,6 +59,8 @@ public class ConfigAutoFetch { private final ConfigUpdateListener retryCallback; private final ScheduledExecutorService scheduledExecutorService; private final Random random; + private final Clock clock; + private final ConfigSharedPrefsClient sharedPrefsClient; private boolean isInBackground; public ConfigAutoFetch( @@ -62,7 +69,8 @@ public ConfigAutoFetch( ConfigCacheClient activatedCache, Set eventListeners, ConfigUpdateListener retryCallback, - ScheduledExecutorService scheduledExecutorService) { + ScheduledExecutorService scheduledExecutorService, + ConfigSharedPrefsClient sharedPrefsClient) { this.httpURLConnection = httpURLConnection; this.configFetchHandler = configFetchHandler; this.activatedCache = activatedCache; @@ -71,6 +79,19 @@ public ConfigAutoFetch( this.scheduledExecutorService = scheduledExecutorService; this.random = new Random(); this.isInBackground = false; + this.sharedPrefsClient = sharedPrefsClient; + this.clock = DefaultClock.getInstance(); + } + + // Increase the backoff duration with a new end time based on Retry Interval + private synchronized void updateBackoffMetadataWithRetryInterval( + int realtimeRetryIntervalInSeconds) { + Date currentTime = new Date(clock.currentTimeMillis()); + long backoffDurationInMillis = realtimeRetryIntervalInSeconds * 1000L; + Date backoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis); + + // Persist the new values to disk-backed metadata. + sharedPrefsClient.setRealtimeBackoffEndTime(backoffEndTime); } private synchronized void propagateErrors(FirebaseRemoteConfigException exception) { @@ -190,6 +211,15 @@ private void handleNotifications(InputStream inputStream) throws IOException { autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion); } } + + // This field in the response indicates that the realtime request should retry after the + // specified interval to establish a long-lived connection. This interval extends the + // backoff duration without affecting the number of retries, so it will not enter an + // exponential backoff state. + if (jsonObject.has(REALTIME_RETRY_INTERVAL)) { + int realtimeRetryIntervalInSeconds = jsonObject.getInt(REALTIME_RETRY_INTERVAL); + updateBackoffMetadataWithRetryInterval(realtimeRetryIntervalInSeconds); + } } catch (JSONException ex) { // Message was mangled up and so it was unable to be parsed. User is notified of this // because it there could be a new configuration that needs to be fetched. @@ -219,15 +249,16 @@ private void autoFetch(int remainingAttempts, long targetVersion) { // Randomize fetch to occur between 0 - 4 seconds. int timeTillFetch = random.nextInt(4); - scheduledExecutorService.schedule( - new Runnable() { - @Override - public void run() { - fetchLatestConfig(remainingAttempts, targetVersion); - } - }, - timeTillFetch, - TimeUnit.SECONDS); + ScheduledFuture unused = + scheduledExecutorService.schedule( + new Runnable() { + @Override + public void run() { + Task unused = fetchLatestConfig(remainingAttempts, targetVersion); + } + }, + timeTillFetch, + TimeUnit.SECONDS); } @VisibleForTesting diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigContainer.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigContainer.java index 92d33eea54d..161139339bc 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigContainer.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigContainer.java @@ -14,6 +14,7 @@ package com.google.firebase.remoteconfig.internal; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -328,11 +329,13 @@ public Builder(ConfigContainer otherContainer) { this.builderRolloutMetadata = otherContainer.getRolloutMetadata(); } + @CanIgnoreReturnValue public Builder replaceConfigsWith(Map configsMap) { this.builderConfigsJson = new JSONObject(configsMap); return this; } + @CanIgnoreReturnValue public Builder replaceConfigsWith(JSONObject configsJson) { try { this.builderConfigsJson = new JSONObject(configsJson.toString()); @@ -345,11 +348,13 @@ public Builder replaceConfigsWith(JSONObject configsJson) { return this; } + @CanIgnoreReturnValue public Builder withFetchTime(Date fetchTime) { this.builderFetchTime = fetchTime; return this; } + @CanIgnoreReturnValue public Builder withAbtExperiments(JSONArray abtExperiments) { try { this.builderAbtExperiments = new JSONArray(abtExperiments.toString()); @@ -362,6 +367,7 @@ public Builder withAbtExperiments(JSONArray abtExperiments) { return this; } + @CanIgnoreReturnValue public Builder withPersonalizationMetadata(JSONObject personalizationMetadata) { try { this.builderPersonalizationMetadata = new JSONObject(personalizationMetadata.toString()); @@ -374,11 +380,13 @@ public Builder withPersonalizationMetadata(JSONObject personalizationMetadata) { return this; } + @CanIgnoreReturnValue public Builder withTemplateVersionNumber(long templateVersionNumber) { this.builderTemplateVersionNumber = templateVersionNumber; return this; } + @CanIgnoreReturnValue public Builder withRolloutMetadata(JSONArray rolloutMetadata) { try { this.builderRolloutMetadata = new JSONArray(rolloutMetadata.toString()); diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java index e130d13df49..3d68b19b68c 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java @@ -77,7 +77,7 @@ public class ConfigFetchHandler { * *

Defined here since {@link HttpURLConnection} does not provide this code. */ - @VisibleForTesting static final int HTTP_TOO_MANY_REQUESTS = 429; + static final int HTTP_TOO_MANY_REQUESTS = 429; /** * First-open time key name in GA user-properties. First-open time is a predefined user-dimension diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java index 7be3ef97136..709702311f9 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java @@ -57,6 +57,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -381,15 +382,16 @@ private synchronized void makeRealtimeHttpConnection(long retryMilliseconds) { if (httpRetriesRemaining > 0) { httpRetriesRemaining--; - scheduledExecutorService.schedule( - new Runnable() { - @Override - public void run() { - beginRealtimeHttpStream(); - } - }, - retryMilliseconds, - TimeUnit.MILLISECONDS); + ScheduledFuture unused = + scheduledExecutorService.schedule( + new Runnable() { + @Override + public void run() { + beginRealtimeHttpStream(); + } + }, + retryMilliseconds, + TimeUnit.MILLISECONDS); } else if (!isInBackground) { propagateErrors( new FirebaseRemoteConfigClientException( @@ -469,7 +471,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { activatedCache, listeners, retryCallback, - scheduledExecutorService); + scheduledExecutorService, + sharedPrefsClient); } // HTTP status code that the Realtime client should retry on. diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java index 7ce24bc44f6..aaa81c10eca 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigSharedPrefsClient.java @@ -373,7 +373,6 @@ Date getBackoffEndTime() { // Realtime exponential backoff logic. // ----------------------------------------------------------------- - @VisibleForTesting public RealtimeBackoffMetadata getRealtimeBackoffMetadata() { synchronized (realtimeBackoffMetadataLock) { return new RealtimeBackoffMetadata( @@ -394,6 +393,15 @@ void setRealtimeBackoffMetadata(int numFailedStreams, Date backoffEndTime) { } } + public void setRealtimeBackoffEndTime(Date backoffEndTime) { + synchronized (realtimeBackoffMetadataLock) { + frcSharedPrefs + .edit() + .putLong(REALTIME_BACKOFF_END_TIME_IN_MILLIS_KEY, backoffEndTime.getTime()) + .apply(); + } + } + void resetRealtimeBackoff() { setRealtimeBackoffMetadata(NO_FAILED_REALTIME_STREAMS, NO_BACKOFF_TIME); } @@ -405,7 +413,6 @@ void resetRealtimeBackoff() { *

The purpose of this class is to avoid race conditions when retrieving backoff metadata * values separately. */ - @VisibleForTesting public static class RealtimeBackoffMetadata { private int numFailedStreams; private Date backoffEndTime; diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt b/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt deleted file mode 100644 index 899381a6515..00000000000 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.remoteconfig.ktx - -import androidx.annotation.Keep -import com.google.firebase.FirebaseApp -import com.google.firebase.components.Component -import com.google.firebase.components.ComponentRegistrar -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ConfigUpdate -import com.google.firebase.remoteconfig.ConfigUpdateListener -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.FirebaseRemoteConfigException -import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings -import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -/** - * Accessing this object for Kotlin apps has changed; see the - * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). - * - * Returns the [FirebaseRemoteConfig] instance of the default [FirebaseApp]. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-config-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -val Firebase.remoteConfig: FirebaseRemoteConfig - get() = FirebaseRemoteConfig.getInstance() - -/** - * Accessing this object for Kotlin apps has changed; see the - * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). - * - * Returns the [FirebaseRemoteConfig] instance of a given [FirebaseApp]. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-config-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = - FirebaseRemoteConfig.getInstance(app) - -/** - * See [FirebaseRemoteConfig#getValue] - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-config-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -operator fun FirebaseRemoteConfig.get(key: String): FirebaseRemoteConfigValue { - return this.getValue(key) -} - -fun remoteConfigSettings( - init: FirebaseRemoteConfigSettings.Builder.() -> Unit -): FirebaseRemoteConfigSettings { - val builder = FirebaseRemoteConfigSettings.Builder() - builder.init() - return builder.build() -} - -/** - * Starts listening for config updates from the Remote Config backend and emits [ConfigUpdate]s via - * a [Flow]. See [FirebaseRemoteConfig.addOnConfigUpdateListener] for more information. - * - * - When the returned flow starts being collected, an [ConfigUpdateListener] will be attached. - * - When the flow completes, the listener will be removed. If there are no attached listeners, the - * connection to the Remote Config backend will be closed. - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-config-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -val FirebaseRemoteConfig.configUpdates - get() = callbackFlow { - val registration = - addOnConfigUpdateListener( - object : ConfigUpdateListener { - override fun onUpdate(configUpdate: ConfigUpdate) { - schedule { trySendBlocking(configUpdate) } - } - - override fun onError(error: FirebaseRemoteConfigException) { - cancel(message = "Error listening for config updates.", cause = error) - } - } - ) - awaitClose { registration.remove() } - } - -/** - * @suppress - * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their - * respective main modules, and the Kotlin extension (KTX) APIs in - * `com.google.firebase:firebase-config-ktx` are now deprecated. As early as April 2024, we'll no - * longer release KTX modules. For details, see the - * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) - */ -@Deprecated( - "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", - ReplaceWith("") -) -@Keep -class FirebaseRemoteConfigKtxRegistrar : ComponentRegistrar { - override fun getComponents(): List> = listOf() -} diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index 9e8f65c767e..35a804b1c74 100644 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -50,6 +50,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.common.util.Clock; +import com.google.android.gms.common.util.DefaultClock; import com.google.android.gms.shadows.common.internal.ShadowPreconditions; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; @@ -104,6 +106,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; @@ -200,6 +203,7 @@ public final class FirebaseRemoteConfigTest { private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + private final Clock clock = DefaultClock.getInstance(); @Before public void setUp() throws Exception { @@ -351,7 +355,8 @@ public void onError(@NonNull FirebaseRemoteConfigException error) { mockActivatedCache, listeners, mockRetryListener, - scheduledExecutorService); + scheduledExecutorService, + sharedPrefsClient); configAutoFetch.setIsInBackground(false); realtimeSharedPrefsClient = new ConfigSharedPrefsClient( @@ -1528,8 +1533,8 @@ public void realtime_stream_listen_backgrounded_disconnects() throws Exception { .closeRealtimeHttpConnection(any(InputStream.class), any(InputStream.class)); when(mockHttpURLConnection.getResponseCode()).thenReturn(200); configRealtimeHttpClientSpy.beginRealtimeHttpStream(); - configRealtimeHttpClientSpy.setIsInBackground(true); flushScheduledTasks(); + configRealtimeHttpClientSpy.setIsInBackground(true); verify(mockHttpURLConnection, times(1)).disconnect(); } @@ -1551,6 +1556,34 @@ public void realtimeStreamListen_andUnableToParseMessage() throws Exception { verify(mockInvalidMessageEventListener).onError(any(FirebaseRemoteConfigClientException.class)); } + @Test + public void realtime_updatesBackoffMetadataWithProvidedRetryInterval() throws Exception { + ConfigRealtimeHttpClient configRealtimeHttpClientSpy = spy(configRealtimeHttpClient); + when(mockHttpURLConnection.getResponseCode()).thenReturn(200); + int expectedRetryIntervalInSeconds = 240; + when(mockHttpURLConnection.getInputStream()) + .thenReturn( + new ByteArrayInputStream( + String.format( + "{ \"latestTemplateVersionNumber\": 1, \"retryIntervalSeconds\": %d }", + expectedRetryIntervalInSeconds) + .getBytes(StandardCharsets.UTF_8))); + when(mockFetchHandler.getTemplateVersionNumber()).thenReturn(1L); + configAutoFetch.listenForNotifications(); + + ArgumentMatcher backoffEndTimeWithinTolerance = + argument -> { + Date currentTime = new Date(clock.currentTimeMillis()); + long backoffDurationInMillis = expectedRetryIntervalInSeconds * 1000L; + Date expectedBackoffEndTime = new Date(currentTime.getTime() + backoffDurationInMillis); + return Math.abs(argument.getTime() - expectedBackoffEndTime.getTime()) + <= TimeUnit.SECONDS.toMillis(1); + }; + + verify(sharedPrefsClient, times(1)) + .setRealtimeBackoffEndTime(argThat(backoffEndTimeWithinTolerance)); + } + @Test public void realtime_stream_listen_get_inputstream_fail() throws Exception { InputStream inputStream = mock(InputStream.class); diff --git a/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt b/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt deleted file mode 100644 index 2a423843a7c..00000000000 --- a/firebase-config/src/test/java/com/google/firebase/remoteconfig/ktx/RemoteConfigTests.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.firebase.remoteconfig.ktx - -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.google.common.util.concurrent.MoreExecutors -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app -import com.google.firebase.ktx.initialize -import com.google.firebase.platforminfo.UserAgentPublisher -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue -import com.google.firebase.remoteconfig.createRemoteConfig -import com.google.firebase.remoteconfig.internal.ConfigCacheClient -import com.google.firebase.remoteconfig.internal.ConfigFetchHandler -import com.google.firebase.remoteconfig.internal.ConfigGetParameterHandler -import com.google.firebase.remoteconfig.internal.ConfigRealtimeHandler -import com.google.firebase.remoteconfig.internal.ConfigSharedPrefsClient -import com.google.firebase.remoteconfig.internal.rollouts.RolloutsStateSubscriptionsHandler -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.RobolectricTestRunner - -const val APP_ID = "1:14368190084:android:09cb977358c6f241" -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") -} - -class StringRemoteConfigValue(val value: String) : DefaultFirebaseRemoteConfigValue() { - 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() - } -} - -@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, - frcSharedPrefs = mock(ConfigSharedPrefsClient::class.java), - realtimeHandler = mock(ConfigRealtimeHandler::class.java), - rolloutsStateSubscriptionsHandler = mock(RolloutsStateSubscriptionsHandler::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) - } -} diff --git a/firebase-config/test-app/README.md b/firebase-config/test-app/README.md index 64149202d50..b8994701353 100644 --- a/firebase-config/test-app/README.md +++ b/firebase-config/test-app/README.md @@ -2,16 +2,16 @@ ## Setup -Download the `google-services.json` file -from [Firebase Console](https://console.firebase.google.com/) (for whatever Firebase project you -have or want to integrate the `test-app`) and store it under the current directory. +Download the `google-services.json` file from +[Firebase Console](https://console.firebase.google.com/) (for whatever Firebase project you have or +want to integrate the `test-app`) and store it under the current directory. Note: The [Package name](https://firebase.google.com/docs/android/setup#register-app) for your app -created on the Firebase Console (for which the `google-services.json` is downloaded) must match -the [applicationId](https://developer.android.com/studio/build/application-id.html) declared in -the `test-app/test-app.gradle.kts` for the app to link to Firebase. +created on the Firebase Console (for which the `google-services.json` is downloaded) must match the +[applicationId](https://developer.android.com/studio/build/application-id.html) declared in the +`test-app/test-app.gradle.kts` for the app to link to Firebase. ## Running -Run the test app directly from Android Studio by selecting and running -the `firebase-config.test-app` run configuration. +Run the test app directly from Android Studio by selecting and running the +`firebase-config.test-app` run configuration. diff --git a/firebase-config/test-app/src/androidTest/kotlin/com/google/firebase/testing/config/FirebaseConfigTest.kt b/firebase-config/test-app/src/androidTest/kotlin/com/google/firebase/testing/config/FirebaseConfigTest.kt index d8b05fac481..3d0234c8895 100644 --- a/firebase-config/test-app/src/androidTest/kotlin/com/google/firebase/testing/config/FirebaseConfigTest.kt +++ b/firebase-config/test-app/src/androidTest/kotlin/com/google/firebase/testing/config/FirebaseConfigTest.kt @@ -18,9 +18,9 @@ package com.google.firebase.testing.config import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.initialize +import com.google.firebase.initialize import org.junit.After import org.junit.Before import org.junit.Test diff --git a/firebase-config/test-app/test-app.gradle.kts b/firebase-config/test-app/test-app.gradle.kts index 0c91cd21fc3..52506f56d9b 100644 --- a/firebase-config/test-app/test-app.gradle.kts +++ b/firebase-config/test-app/test-app.gradle.kts @@ -1,7 +1,4 @@ @file:Suppress("DEPRECATION") // App projects should still use FirebaseTestLabPlugin. - -import com.google.firebase.gradle.plugins.ci.device.FirebaseTestLabPlugin - /* * Copyright 2023 Google LLC * @@ -18,6 +15,9 @@ import com.google.firebase.gradle.plugins.ci.device.FirebaseTestLabPlugin * limitations under the License. */ +import com.google.firebase.gradle.plugins.ci.device.FirebaseTestLabPlugin +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -47,29 +47,28 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { jvmTarget = "1.8" } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } + dependencies { implementation(project(":firebase-crashlytics")) { exclude(group = "com.google.firebase", module = "firebase-config-interop") } - implementation(project(":firebase-config")) { - exclude(group = "com.google.firebase", module = "firebase-config-interop") - } - implementation(project(":firebase-config:ktx")) + implementation(project(":firebase-config")) // This is required since a `project` dependency on frc does not expose the APIs of its // "implementation" dependencies. The alternative would be to make common an "api" dep of // remote-config. // Released artifacts don't need these dependencies since they don't use `project` to refer // to Remote Config. - implementation("com.google.firebase:firebase-common:21.0.0") - implementation("com.google.firebase:firebase-common-ktx:21.0.0") - implementation("com.google.firebase:firebase-components:18.0.0") + implementation("com.google.firebase:firebase-common:22.0.0") + implementation(libs.firebase.components) implementation("com.google.firebase:firebase-installations-interop:17.1.0") - runtimeOnly("com.google.firebase:firebase-installations:17.1.4") + runtimeOnly("com.google.firebase:firebase-installations:18.0.0") { + exclude(group = "com.google.firebase", module = "firebase-common-ktx") + } implementation("com.google.android.gms:play-services-basement:18.1.0") implementation("com.google.android.gms:play-services-tasks:18.0.1") @@ -80,7 +79,6 @@ dependencies { implementation("androidx.core:core-ktx:1.9.0") implementation("com.google.android.material:material:1.8.0") - androidTestImplementation("com.google.firebase:firebase-common-ktx:21.0.0") androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.truth) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index 38387afdd12..369baea1949 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,73 +1,113 @@ # Unreleased -* [changed] Updated `firebase-crashlytics` dependency to v19.4.4 +# 20.0.3 + +- [changed] Bumped internal dependencies. + +# 20.0.2 + +- [changed] Bumped internal dependencies. + +# 20.0.1 + +- [changed] Updated `firebase-crashlytics` dependency to 20.0.1 + +# 20.0.0 + +- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. + +# 19.4.4 + +- [changed] Updated `firebase-crashlytics` dependency to v19.4.4 # 19.4.3 -* [changed] Updated internal Crashpad version to commit `21a20e`. + +- [changed] Updated internal Crashpad version to commit `21a20e`. # 19.4.2 -* [changed] Updated `firebase-crashlytics` dependency to v19.4.2 + +- [changed] Updated `firebase-crashlytics` dependency to v19.4.2 # 19.4.1 -* [changed] Updated `firebase-crashlytics` dependency to v19.4.1 + +- [changed] Updated `firebase-crashlytics` dependency to v19.4.1 # 19.3.0 -* [changed] Updated `firebase-crashlytics` dependency to v19.3.0 + +- [changed] Updated `firebase-crashlytics` dependency to v19.3.0 # 19.2.1 -* [changed] Updated `firebase-crashlytics` dependency to v19.2.1 + +- [changed] Updated `firebase-crashlytics` dependency to v19.2.1 # 19.2.0 -* [changed] Updated `firebase-crashlytics` dependency to v19.2.0 + +- [changed] Updated `firebase-crashlytics` dependency to v19.2.0 # 19.1.0 -* [changed] Updated `firebase-crashlytics` dependency to v19.1.0 + +- [changed] Updated `firebase-crashlytics` dependency to v19.1.0 # 19.0.3 -* [changed] Updated `firebase-crashlytics` dependency to v19.0.3 + +- [changed] Updated `firebase-crashlytics` dependency to v19.0.3 # 19.0.2 -* [changed] Update libcrashlytics to support 16 kb page sizes. + +- [changed] Update libcrashlytics to support 16 kb page sizes. # 19.0.1 -* [changed] Updated `firebase-crashlytics` dependency to v19.0.1 + +- [changed] Updated `firebase-crashlytics` dependency to v19.0.1 # 19.0.0 -* [changed] Bump internal dependencies + +- [changed] Bump internal dependencies # 18.6.3 -* [changed] Updated `firebase-crashlytics` dependency to v18.6.3 + +- [changed] Updated `firebase-crashlytics` dependency to v18.6.3 # 18.6.0 -* [changed] Updated `firebase-crashlytics` dependency to v18.6.0 + +- [changed] Updated `firebase-crashlytics` dependency to v18.6.0 # 18.5.0 -* [changed] Updated `firebase-crashlytics` dependency to v18.5.0 + +- [changed] Updated `firebase-crashlytics` dependency to v18.5.0 # 18.4.3 -* [changed] Updated `firebase-crashlytics` dependency to v18.4.3 + +- [changed] Updated `firebase-crashlytics` dependency to v18.4.3 # 18.4.2 -* [changed] Updated `firebase-crashlytics` dependency to v18.4.2 + +- [changed] Updated `firebase-crashlytics` dependency to v18.4.2 # 18.4.1 -* [changed] Updated `firebase-crashlytics` dependency to v18.4.1 + +- [changed] Updated `firebase-crashlytics` dependency to v18.4.1 # 18.4.0 -* [changed] Updated `firebase-crashlytics` dependency to v18.4.0 + +- [changed] Updated `firebase-crashlytics` dependency to v18.4.0 # 18.3.7 -* [changed] Updated `firebase-crashlytics` dependency to v18.3.7 + +- [changed] Updated `firebase-crashlytics` dependency to v18.3.7 # 18.3.6 -* [changed] Updated `firebase-crashlytics` dependency to v18.3.6. + +- [changed] Updated `firebase-crashlytics` dependency to v18.3.6. # 18.3.5 -* [fixed] Updated `firebase-common` to its latest version (v20.3.0) to fix an - issue that was causing a nondeterministic crash on startup. -* [changed] Updated `firebase-crashlytics` dependency to v18.3.5. + +- [fixed] Updated `firebase-common` to its latest version (v20.3.0) to fix an issue that was causing + a nondeterministic crash on startup. +- [changed] Updated `firebase-crashlytics` dependency to v18.3.5. # 18.3.4 +