diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f1834b3d1c..85c083b61c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -13,7 +13,7 @@ on: env: CCACHE_DIR: ${{ github.workspace }}/ccache_dir GITHUB_TOKEN: ${{ github.token }} - xcodeVersion: "14.1" # Only affects Mac runners, and only for prerequisites. + xcodeVersion: "16.2" # Only affects Mac runners, and only for prerequisites. concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }} @@ -50,13 +50,30 @@ jobs: os: ${{ fromJson(needs.prepare_matrix.outputs.matrix_os) }} architecture: ${{ fromJson(needs.prepare_matrix.outputs.matrix_architecture) }} python_version: ${{ fromJson(needs.prepare_matrix.outputs.matrix_python_version) }} + exclude: + # Do not attempt to use arm64 on Windows or Linux. + - os: windows-latest + architecture: arm64 + - os: ubuntu-22.04 + architecture: arm64 + # Do not attempt to use x64 on Mac. + - os: macos-14 + architecture: x64 steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: setup Xcode version (macos) if: runner.os == 'macOS' run: sudo xcode-select -s /Applications/Xcode_${{ env.xcodeVersion }}.app/Contents/Developer - name: Force Java 11 + if: runner.os != 'macOS' shell: bash run: echo "JAVA_HOME=${JAVA_HOME_11_X64}" >> $GITHUB_ENV + - name: Force Java 11 (mac) + if: runner.os == 'macOS' + shell: bash + run: echo "JAVA_HOME=${JAVA_HOME_11_arm64}" >> $GITHUB_ENV - name: Store git credentials for all git commands # Forces all git commands to use authenticated https, to prevent throttling. shell: bash diff --git a/.github/workflows/build-report.yml b/.github/workflows/build-report.yml index 9af62a6bf8..3fdb21d135 100644 --- a/.github/workflows/build-report.yml +++ b/.github/workflows/build-report.yml @@ -5,6 +5,8 @@ on: schedule: - cron: "0 21 * * *" # 9pm UTC = 1pm PST / 2pm PDT, 12 hours after testapps run +permissions: write-all + env: GITHUB_TOKEN: ${{ github.token }} numDays: 7 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c1ee5375f0..06b3422adf 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -4,6 +4,8 @@ on: pull_request: types: [opened, reopened, synchronize, labeled, unlabeled] +permissions: write-all + env: triggerLabelFull: "tests-requested: full" triggerLabelQuick: "tests-requested: quick" @@ -51,6 +53,9 @@ jobs: # This check succeeds if Doxygen documentation generates without errors. runs-on: ubuntu-22.04 steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: submodules: false diff --git a/.github/workflows/checks_secure.yml b/.github/workflows/checks_secure.yml index c919b374b6..9bd167d0b5 100644 --- a/.github/workflows/checks_secure.yml +++ b/.github/workflows/checks_secure.yml @@ -5,6 +5,8 @@ on: pull_request_target: types: [synchronize] +permissions: write-all + concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true diff --git a/.github/workflows/cpp-packaging.yml b/.github/workflows/cpp-packaging.yml index 706a30515a..d31257572a 100644 --- a/.github/workflows/cpp-packaging.yml +++ b/.github/workflows/cpp-packaging.yml @@ -32,9 +32,9 @@ env: demumbleVer: "df938e45c2b0e064fb5323d88b692d03b451d271" # Use SHA256 for hashing files. hashCommand: "sha256sum" - # Xcode version 15.1 is the version we build the SDK with. + # Xcode version 16.2 is the version we build the SDK with. # Our MacOS runners will use the version in /Applications/Xcode_${xcodeVersion}.app - xcodeVersion: "15.1" + xcodeVersion: "16.2" # LLVM version with ARM MachO support has no version number yet. llvmVer: "5f187f0afaad33013ba03454c4749d99b1362534" GITHUB_TOKEN: ${{ github.token }} @@ -79,17 +79,20 @@ jobs: if: ${{ github.event.inputs.downloadPublicVersion == '' && github.event.inputs.downloadPreviousRun == '' }} strategy: matrix: - os: [ubuntu-22.04, macos-13] + os: [ubuntu-22.04, macos-14] include: - os: ubuntu-22.04 tools_platform: linux # Binutils 2.35.1 released Sep 19, 2020 binutils_version: "2.35.1" - - os: macos-13 + - os: macos-14 tools_platform: darwin # Binutils 2.35.1 released Sep 19, 2020 binutils_version: "2.35.1" steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: setup Xcode version (macos) if: runner.os == 'macOS' run: sudo xcode-select -s /Applications/Xcode_${{ env.xcodeVersion }}.app/Contents/Developer @@ -185,9 +188,12 @@ jobs: build_and_package_ios_tvos: name: build-and-package-ios-tvos - runs-on: macos-13 + runs-on: macos-14 if: ${{ github.event.inputs.downloadPublicVersion == '' && github.event.inputs.downloadPreviousRun == '' }} steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: Store git credentials for all git commands # Forces all git commands to use authenticated https, to prevent throttling. shell: bash @@ -248,6 +254,9 @@ jobs: strategy: fail-fast: false steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: Force Java 11 shell: bash run: echo "JAVA_HOME=${JAVA_HOME_11_X64}" >> $GITHUB_ENV @@ -308,7 +317,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-latest, ubuntu-22.04, macos-13] + os: [windows-latest, ubuntu-22.04, macos-14] build_type: ["Release", "Debug"] architecture: ["x64", "x86", "arm64"] msvc_runtime: ["static", "dynamic"] @@ -326,7 +335,7 @@ jobs: vcpkg_triplet_suffix: "linux" additional_build_flags: "" sdk_platform: "linux" - - os: macos-13 + - os: macos-14 vcpkg_triplet_suffix: "osx" additional_build_flags: "--target_format libraries" sdk_platform: "darwin" @@ -334,13 +343,13 @@ jobs: exclude: - os: windows-latest linux_abi: "c++11" - - os: macos-13 + - os: macos-14 architecture: "x86" - - os: macos-13 + - os: macos-14 msvc_runtime: "dynamic" - - os: macos-13 + - os: macos-14 linux_abi: "c++11" - - os: macos-13 + - os: macos-14 build_type: "Debug" - os: ubuntu-22.04 msvc_runtime: "dynamic" @@ -352,6 +361,9 @@ jobs: architecture: "arm64" steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: Store git credentials for all git commands # Forces all git commands to use authenticated https, to prevent throttling. shell: bash @@ -492,7 +504,7 @@ jobs: suffix: '-x64-Debug-dynamic' runs_on_platform: ubuntu-22.04 - sdk_platform: darwin - runs_on_platform: macos-13 + runs_on_platform: macos-14 exclude: - sdk_platform: windows suffix: '' diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index fa1b792cad..5aad61c385 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -72,11 +72,13 @@ jobs: # msvc_runtime excludes - os: ubuntu-22.04 msvc_runtime: "dynamic" - - os: macos-13 + - os: macos-14 msvc_runtime: "dynamic" # architecture excluees - - os: macos-13 + - os: macos-14 architecture: "x86" + - os: macos-14 + architecture: "x64" # Xcode excludes -- allow only one on osx and linux - os: ubuntu-22.04 xcode_version: "11.7" @@ -95,6 +97,9 @@ jobs: - xcode_version: "11.7" architecture: "arm64" steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: Store git credentials for all git commands # Forces all git commands to use authenticated https, to prevent throttling. shell: bash @@ -165,9 +170,17 @@ jobs: - name: Setup python uses: actions/setup-python@v4 + if: startsWith(matrix.os, 'ubuntu') with: python-version: ${{ matrix.python_version }} - architecture: 'x64' + architecture: x64 + + - name: Setup python (Mac) + uses: actions/setup-python@v4 + if: startsWith(matrix.os, 'macos') + with: + python-version: ${{ matrix.python_version }} + architecture: ${{ matrix.architecture }} - name: Install Desktop SDK prerequisites uses: nick-invision/retry@v2 @@ -296,6 +309,9 @@ jobs: strategy: fail-fast: false steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: ref: ${{needs.check_and_prepare.outputs.github_ref}} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index ac88fb96dd..0522e51dfc 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -17,11 +17,11 @@ on: required: true apis: description: 'CSV of apis to build and test' - default: 'analytics,app_check,auth,database,dynamic_links,firestore,functions,gma,installations,messaging,remote_config,storage' + default: 'analytics,app_check,auth,database,dynamic_links,firestore,functions,installations,messaging,remote_config,storage,ump' required: true operating_systems: description: 'CSV of VMs to run on' - default: 'ubuntu-22.04,windows-latest,macos-13' + default: 'ubuntu-22.04,windows-latest,macos-14' required: true desktop_ssl_variants: description: 'CSV of desktop SSL variants to use' @@ -49,14 +49,9 @@ env: triggerLabelFull: "tests-requested: full" triggerLabelQuick: "tests-requested: quick" pythonVersion: '3.8' - xcodeVersion: '15.1' + xcodeVersion: '16.2' artifactRetentionDays: 2 GITHUB_TOKEN: ${{ github.token }} - # All self-hosted ARM Mac runners should have this label. Due to how - # our custom reporting works, it must be exactly two words separated - # by a hyphen. The first word must be "macos". The second word will - # be omitted from the summary log. - runnerLabelMacArm64: "macos-m1custom" jobs: check_and_prepare: @@ -77,8 +72,6 @@ jobs: xcode_version: ${{ steps.matrix_config.outputs.xcode_version }} ios_device: ${{ steps.matrix_config.outputs.ios_device }} tvos_device: ${{ steps.matrix_config.outputs.tvos_device }} - # Copy the runner label here because matrix specifiers cannot see env. - runner_label_macos_arm64: ${{ env.runnerLabelMacArm64 }} steps: ### Fail the workflow if the user does not have admin access to run the tests. - name: Check if user has permission to trigger tests @@ -192,7 +185,7 @@ jobs: # list. Then we can use fromJson to define the field in the matrix for the tests job. if [[ "${{ github.event.schedule }}" == "0 9 * * *" ]]; then # at 1am PST/2am PDT. Running integration tests and generate test report for all testapps except firestore - apis="analytics,app_check,auth,database,dynamic_links,functions,gma,installations,messaging,remote_config,storage" + apis="analytics,app_check,auth,database,dynamic_links,functions,installations,messaging,remote_config,storage,ump" echo "::warning ::Running main nightly tests" elif [[ "${{ github.event.schedule }}" == "0 10 * * *" || "${{ github.event.schedule }}" == "0 11 * * *" ]]; then # at 2am PST/3am PDT and 3am PST/4am PDT. Running integration tests for firestore and generate test report. @@ -206,7 +199,7 @@ jobs: # at 3am PST/4am PDT. Running firestore desktop integration test aginst tip-of-tree ios repo echo "::warning ::Running against Firestore tip-of-tree" matrix_platform="Desktop" - matrix_os=$( python scripts/gha/print_matrix_configuration.py -w integration_tests ${TEST_MATRIX_PARAM} -k os -o "ubuntu-22.04,macos-13") + matrix_os=$( python scripts/gha/print_matrix_configuration.py -w integration_tests ${TEST_MATRIX_PARAM} -k os -o "ubuntu-22.04,macos-14") else matrix_platform=$( python scripts/gha/print_matrix_configuration.py -w integration_tests ${TEST_MATRIX_PARAM} -k platform -o "${{github.event.inputs.platforms}}" --apis ${apis} ) matrix_os=$( python scripts/gha/print_matrix_configuration.py -w integration_tests ${TEST_MATRIX_PARAM} -k os -o "${{github.event.inputs.operating_systems}}") @@ -273,19 +266,22 @@ jobs: - os: ubuntu-22.04 arch: arm64 # Do not attempt to use x86 on Mac. - - os: macos-13 + - os: macos-14 arch: x86 # Until we support building openssl from source, we can't use the - # system's openssl when cross-compiling, except on Linux. Builds all - # happen on x64 machines, so arm64 and x86 are technically - # cross-compiling. + # system's openssl when cross-compiling, except on Linux. Builds on Linux + # happen on x64 machines, so x86 is technically cross-compiling. Builds on + # Mac happen on arm64 machines, so x64 is technically cross-compiling. - os: windows-latest ssl_variant: openssl arch: x86 - - os: macos-13 + - os: macos-14 ssl_variant: openssl - arch: arm64 + arch: x64 steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: ref: ${{needs.check_and_prepare.outputs.github_ref}} @@ -350,7 +346,7 @@ jobs: elif [[ "${{ github.event.inputs.firestore_dep_source }}" ]]; then additional_flags+=(--cmake_flag=-DFIRESTORE_DEP_SOURCE=${{ github.event.inputs.firestore_dep_source }}) fi - python scripts/gha/build_testapps.py --p Desktop \ + VERBOSE=1 python scripts/gha/build_testapps.py --p Desktop \ --t ${{ needs.check_and_prepare.outputs.apis }} \ --output_directory "${{ github.workspace }}" \ --artifact_name "desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.ssl_variant }}" \ @@ -424,13 +420,21 @@ jobs: matrix: os: ${{ fromJson(needs.check_and_prepare.outputs.matrix_os) }} steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: ref: ${{needs.check_and_prepare.outputs.github_ref}} submodules: true - name: Force Java 11 + if: ${{ !(runner.os == 'macOS') }} shell: bash run: echo "JAVA_HOME=${JAVA_HOME_11_X64}" >> $GITHUB_ENV + - name: Force Java 11 (mac) + if: ${{ runner.os == 'macOS' }} + shell: bash + run: echo "JAVA_HOME=${JAVA_HOME_11_arm64}" >> $GITHUB_ENV - name: Add msbuild to PATH (Windows) if: startsWith(matrix.os, 'windows') uses: microsoft/setup-msbuild@v1.1 @@ -533,8 +537,11 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-13] + os: [macos-14] steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: ref: ${{needs.check_and_prepare.outputs.github_ref}} @@ -638,8 +645,11 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-13] + os: [macos-14] steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - uses: actions/checkout@v3 with: ref: ${{needs.check_and_prepare.outputs.github_ref}} @@ -755,20 +765,18 @@ jobs: - os: ubuntu-22.04 arch: arm64 # Do not attempt to use x86 on Mac. - - os: macos-13 + - os: macos-14 arch: x86 # Until we support building openssl from source, we can't use the - # system's openssl when cross-compiling, except on Linux. Builds all - # happen on x64 machines, so arm64 and x86 are technically - # cross-compiling. + # system's openssl when cross-compiling, except on Linux. Builds on Linux + # happen on x64 machines, so x86 is technically cross-compiling. Builds on + # Mac happen on arm64 machines, so x64 is technically cross-compiling. - os: windows-latest ssl_variant: openssl arch: x86 - # Custom for this matrix: MacOS GitHub-hosted runner cannot test arm64 - # code. Exclude that scenario from running here; it will run in - # test_desktop_custom_runners. - - os: macos-13 - arch: arm64 + - os: macos-14 + ssl_variant: openssl + arch: x64 steps: - uses: actions/checkout@v3 with: @@ -913,7 +921,7 @@ jobs: distribution: 'temurin' java-version: '11' - name: Run Android integration tests on Emulator locally - timeout-minutes: 180 + timeout-minutes: 240 if: steps.device-info.outputs.device_type == 'virtual' run: | python scripts/gha/test_simulator.py --testapp_dir testapps \ @@ -998,7 +1006,7 @@ jobs: test_ios: name: test-ios-${{ matrix.build_os }}-${{ matrix.ios_device }}-${{ matrix.test_type }} needs: [check_and_prepare, build_ios] - runs-on: macos-13 + runs-on: macos-14 if: contains(needs.check_and_prepare.outputs.matrix_platform, 'iOS') && needs.check_and_prepare.outputs.apis != '' && !cancelled() strategy: fail-fast: false @@ -1012,7 +1020,7 @@ jobs: test_type: "uitest" - ios_device: "ios_latest" test_type: "uitest" - build_os: [macos-13] + build_os: [macos-14] steps: - uses: actions/checkout@v3 with: @@ -1090,7 +1098,7 @@ jobs: security list-keychains -d user -s tmp-keychain security default-keychain -s tmp-keychain - name: Run iOS integration tests on Simulator locally - timeout-minutes: 180 + timeout-minutes: 240 if: steps.device-info.outputs.device_type == 'virtual' run: | python scripts/gha/test_simulator.py --testapp_dir testapps \ @@ -1101,7 +1109,7 @@ jobs: - id: ftl_test if: steps.device-info.outputs.device_type == 'ftl' uses: FirebaseExtended/github-actions/firebase-test-lab@v1.4 - timeout-minutes: 120 + timeout-minutes: 180 with: credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CREDENTIALS }} testapp_dir: testapps @@ -1177,13 +1185,13 @@ jobs: test_tvos: name: test-tvos-${{ matrix.build_os }}-${{ matrix.tvos_device }} needs: [check_and_prepare, build_tvos] - runs-on: macos-13 + runs-on: macos-14 if: contains(needs.check_and_prepare.outputs.matrix_platform, 'tvOS') && needs.check_and_prepare.outputs.apis != '' && !cancelled() strategy: fail-fast: false matrix: tvos_device: ${{ fromJson(needs.check_and_prepare.outputs.tvos_device) }} - build_os: [macos-13] + build_os: [macos-14] steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 45dbe79caf..7dabccfe11 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -41,9 +41,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ 'macos-13' ] + os: [ 'macos-14' ] xcode_version: ${{ fromJson(needs.prepare_matrix.outputs.matrix_xcode_version) }} steps: + - uses: lukka/get-cmake@latest + with: + cmakeVersion: "~3.31.0" - name: Store git credentials for all git commands # Forces all git commands to use authenticated https, to prevent throttling. shell: bash @@ -63,7 +66,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: 3.8 - architecture: "x64" + architecture: "arm64" - name: Install prerequisites run: | diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 042a9f63ae..3a0fd70d48 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -8,9 +8,6 @@ on: updateiOS: description: 'update iOS dependencies?' default: 1 - includeGMA: - description: 'include GMA?' - default: 0 triggerTests: description: 'trigger tests on PR?' default: 1 @@ -30,7 +27,7 @@ env: jobs: update_dependencies: name: update-deps - runs-on: macos-13 + runs-on: macos-14 steps: - name: Get token for firebase-workflow-trigger uses: tibdex/github-app-token@v1 @@ -68,20 +65,16 @@ jobs: - name: Run update script run: | - gma_flag= - if [[ ${{ github.event.inputs.includeGMA }} -eq 1 ]]; then - gma_flag="--include_gma" - fi if [[ ${{ github.event.inputs.updateiOS }} -eq 1 ]]; then if [[ ${{ github.event.inputs.updateAndroid }} -eq 1 ]]; then # Update both echo "Updating all dependencies" - python scripts/update_android_ios_dependencies.py --logfile=${UPDATE_LOGFILE} ${gma_flag} + python scripts/update_android_ios_dependencies.py --logfile=${UPDATE_LOGFILE} echo "CHOSEN_DEPS=mobile" >> $GITHUB_ENV else # Update iOS only echo "Updating iOS dependencies only" - python scripts/update_android_ios_dependencies.py --skip_android --logfile=${UPDATE_LOGFILE} ${gma_flag} + python scripts/update_android_ios_dependencies.py --skip_android --logfile=${UPDATE_LOGFILE} echo "CHOSEN_DEPS=iOS" >> $GITHUB_ENV fi # iOS: Update Firestore external version to match Firestore Cocoapod version. @@ -165,7 +158,7 @@ jobs: elif [[ ${{ github.event.inputs.updateAndroid }} -eq 1 ]]; then # Update Android only echo "Updating Android dependencies only" - python scripts/update_android_ios_dependencies.py --skip_ios --logfile=${UPDATE_LOGFILE} ${gma_flag} + python scripts/update_android_ios_dependencies.py --skip_ios --logfile=${UPDATE_LOGFILE} echo "CHOSEN_DEPS=Android" >> $GITHUB_ENV else echo "::error ::Neither Android nor iOS selected. Exiting." diff --git a/.github/workflows/update-feature-branches.yml b/.github/workflows/update-feature-branches.yml index b39ad3e279..20590ab7d2 100644 --- a/.github/workflows/update-feature-branches.yml +++ b/.github/workflows/update-feature-branches.yml @@ -13,6 +13,8 @@ on: schedule: - cron: "0 16 * * 1" # Mondays, 4pm UTC = 9am PST / 10am PDT +permissions: write-all + env: defaultBranchPattern: "feature_branch/*" defaultMainBranch: "main" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..c35a7e8958 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,679 @@ +# Introduction + +> **Note on Document Formatting:** This document (`AGENTS.md`) should be +> maintained with lines word-wrapped to a maximum of 80 characters to ensure +> readability across various editors and terminals. + +This document provides context and guidance for AI agents (like Jules) when +making changes to the Firebase C++ SDK repository. It covers essential +information about the repository's structure, setup, testing procedures, API +surface, best practices, and common coding patterns. + +For a detailed view of which Firebase products are supported on each C++ +platform (Android, iOS, tvOS, macOS, Windows, Linux), refer to the official +[Firebase library support by platform table](https://firebase.google.com/docs/cpp/learn-more#library-support-by-platform). + +The Firebase C++ SDKs for desktop platforms (Windows, Linux, macOS) are +entirely open source and hosted in the main `firebase/firebase-cpp-sdk` GitHub +repository. The C++ SDKs for mobile platforms (iOS, tvOS, Android) are built +on top of the respective native open-source Firebase SDKs (Firebase iOS SDK and +Firebase Android SDK). + +The goal is to enable agents to understand the existing conventions and +contribute effectively to the codebase. + +# Setup Commands + +## Prerequisites + +Before building the Firebase C++ SDK, ensure the following prerequisites are +installed. Refer to the main `README.md` for detailed installation +instructions for your specific platform. + +* **CMake**: Version 3.7 or newer. +* **Python**: Version 3.7 or newer. +* **Abseil-py**: Python package. +* **OpenSSL**: Required for desktop builds, unless you build with the + `-DFIREBASE_USE_BORINGSSL=YES` cmake flag. +* **libsecret-1-dev**: (Linux Desktop) Required for secure credential storage. + Install using `sudo apt-get install libsecret-1-dev`. +* **Android SDK & NDK**: Required for building Android libraries. `sdkmanager` + can be used for installation. CMake for Android (version 3.10.2 + recommended) is also needed. +* **(Windows Only) Strings**: From Microsoft Sysinternals, required for + Android builds on Windows. +* **Cocoapods**: Required for building iOS or tvOS libraries. + +To build for Desktop, you can install prerequisites by running the following +script in the root of the repository: `scripts/gha/install_prereqs_desktop.py` + +To build for Android, you can install prerequisites by running the following +script in the root of the repository: `build_scripts/android/install_prereqs.sh` + +## Building the SDK + +The SDK uses CMake for C++ compilation and Gradle for Android-specific parts. + +### CMake (Desktop, iOS, tvOS) + +1. Create a build directory (e.g., `mkdir desktop_build && cd desktop_build`). +2. Run CMake to configure: `cmake ..` + * For Desktop: Run as is. You can use BORINGSSL instead of OpenSSL (for fewer + system dependencies with the `-DFIREBASE_USE_BORINGSSL=YES` parameter. + * For iOS, include the `-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/ios.cmake` + parameter. This requires running on a Mac build machine. +3. Build specific targets: `cmake --build . --target firebase_analytics` + (replace `firebase_analytics` with the desired library). + Or omit the entire `--target` parameter to build all targets. + + For development, building specific targets + (e.g., `cmake --build . --target firebase_app`) is generally faster and + recommended once CMake configuration is complete. The full build + (`cmake --build .`) can be very time-consuming (but can be sped up by adding + `-j4` to the command-line). + +You can also use the `scripts/gha/build_desktop.py` script to build the full +desktop SDK. + +Refer to `README.md` for details on CMake generators and providing custom +third-party dependency locations. + +### Gradle (Android) + +Each Firebase C++ library is a Gradle subproject. To build a specific library +(e.g., Analytics): + +```bash +./gradlew :analytics:assembleRelease +``` + +This command should be run from the root of the repository. Proguard files are +generated in each library's build directory (e.g., +`analytics/build/analytics.pro`). + +You can build the entire SDK for Android by running `./gradlew build` or +`build_scripts/android/build.sh`. + +### Xcode (iOS) + +Unfortunately, the iOS version of the SDK cannot be built on Linux, it can only +be built in a MacOS environment. You will have to rely on GitHub Actions to +build for iOS, and have the user inform you of any build issues that come up. + +### Troubleshooting Desktop Builds + +* Linux: **Missing `libsecret-1-dev`**: + CMake configuration may fail if `libsecret-1-dev` is not installed. + The `scripts/gha/install_prereqs_desktop.py` script should handle this. + If it doesn't, or if the package is removed, you might need to install it + manually: `sudo apt-get update && sudo apt-get install -y libsecret-1-dev`. + +* Linux: **LevelDB Patch Failure when building Firestore**: + If you are building the SDK with Firestore enabled + (`-DFIREBASE_INCLUDE_FIRESTORE=ON`, which is the default for desktop) and + encounter a patch error related to `leveldb-1.23_windows_paths.patch` (e.g., + `util/env_windows.cc: patch does not apply`), you can ignore this issue if + it does not prevent the rest of the build from running. The patch is only + important on Windows. + +Common system library dependencies for desktop: +* **Windows**: Common dependencies include `advapi32.lib`, `ws2_32.lib`, + `crypt32.lib`. Specific products might need others (e.g., Firestore: + `rpcrt4.lib`, `ole32.lib`, `shell32.lib`). +* **macOS**: Common dependencies include `pthread` (system library) and + frameworks like `CoreFoundation`, `Foundation`, and `Security`. +* **Linux**: Common dependencies include `pthread` (system library). When + using GCC 5+, define `-D_GLIBCXX_USE_CXX11_ABI=0`. + +On all desktop platforms, building with -DFIREBASE_USE_BORINGSSL=YES can help +bypass any OpenSSL dependency issues. + +## Including the SDK in Projects + +### CMake Projects + +Use `add_subdirectory()` in your `CMakeLists.txt`: + +```cmake +add_subdirectory("[[Path to the Firebase C++ SDK]]") +target_link_libraries([[Your CMake Target]] firebase_analytics firebase_app) +``` + +### Android Gradle Projects + +In addition to CMake setup, use `Android/firebase_dependencies.gradle` in your +`build.gradle`: + +```gradle +apply from: "[[Path to the Firebase C++ SDK]]/Android/firebase_dependencies.gradle" +firebaseCpp.dependencies { + analytics +} +``` + +For more detailed instructions and examples, always refer to the main +`README.md` and the +[C++ Quickstarts](https://github.com/firebase/quickstart-cpp). + +# Testing + +## Testing Strategy + +The primary method for testing in this repository is through **integration +tests** for each Firebase library. While the `README.md` mentions unit tests +run via CTest, the current and preferred approach is to ensure comprehensive +coverage within the integration tests. + +## Running Tests + +* **Integration Test Location**: Integration tests for each Firebase product + (e.g., Firestore, Auth) are typically located in the `integration_test/` + directory within that product's module (e.g., + `firestore/integration_test/`). + + Because building integration tests requires internal google-services files, + Jules cannot do it in its environment; instead, we rely on GitHub Actions's + Integration Test workflow to build and run the integration tests. + +## Writing Tests + +When adding new features or fixing bugs: + +* Prioritize adding or updating integration tests within the respective + product's `integration_test/` directory. +* Ensure tests cover the new functionality thoroughly and verify interactions + with the Firebase backend or other relevant components. +* Follow existing patterns within the integration tests for consistency. + +# API Surface + +## General API Structure + +The Firebase C++ SDK exposes its functionality through a set of classes and +functions organized by product (e.g., Firestore, Authentication, Realtime +Database). + +### Initialization + +1. **`firebase::App`**: This is the central entry point for the SDK. + * It must be initialized first using `firebase::App::Create(...)`. + * On Android, this requires passing the JNI environment (`JNIEnv*`) and + the Android Activity (`jobject`). + * `firebase::AppOptions` can be used to configure the app with specific + parameters if not relying on a `google-services.json` or + `GoogleService-Info.plist` file. +2. **Service Instances**: Once `firebase::App` is initialized, you generally + obtain instances of specific Firebase services using a static + `GetInstance()` method on the service's class, passing the `firebase::App` + object. + * Examples for services like Auth, Database, Storage, Firestore: + * `firebase::auth::Auth* auth = firebase::auth::Auth::GetAuth(app, &init_result);` + * `firebase::database::Database* database = firebase::database::Database::GetInstance(app, &init_result);` + * `firebase::storage::Storage* storage = firebase::storage::Storage::GetInstance(app, &init_result);` + * Always check the `init_result` (an `InitResult` enum, often + `firebase::kInitResultSuccess` on success) to ensure these services + were initialized successfully. + * **Note on Analytics**: Some products, like Firebase Analytics, have a + different pattern. Analytics is typically initialized with + `firebase::analytics::Initialize(const firebase::App& app)` (often + handled automatically for the default `App` instance). After this, + Analytics functions (e.g., `firebase::analytics::LogEvent(...)`) are + called as global functions within the `firebase::analytics` namespace, + rather than on an instance object obtained via `GetInstance()`. + Refer to the specific product's header file for its exact + initialization mechanism if it deviates from the common + `GetInstance(app, ...)` pattern. + +### Asynchronous Operations: `firebase::Future` + +All asynchronous operations in the SDK return a `firebase::Future` object, +where `T` is the type of the expected result. + +* **Status Checking**: Use `future.status()` to check if the operation is + `kFutureStatusPending`, `kFutureStatusComplete`, or + `kFutureStatusInvalid`. +* **Getting Results**: Once `future.status() == kFutureStatusComplete`: + * Check for errors: `future.error()`. A value of `0` (e.g., + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`) usually indicates success. + * Get the error message: `future.error_message()`. + * Get the result: `future.result()`. This returns a pointer to the result + object of type `T`. The result is only valid if `future.error()` + indicates success. +* **Completion Callbacks**: Use `future.OnCompletion(...)` to register a + callback function (lambda or function pointer) that will be invoked when + the future completes. The callback receives the completed future as an + argument. +* k?????Fn_* enums: A list of each SDK's asynchronous functions is usually + kept in an enum in that SDK. For example, all of Auth's asynchronous + functions are named kAuthFn_* and kUserFn_*. Only asynchronous operations + (which return a Future) need to be in those function enums; these are used + internally to hold a reference to the FutureHandle for the *LastResult() + methods. If you add a new asynchronous operation, it should be added to + that enum, and that ID should be used for all of the internal FutureApi + operations. Non-async functions never need to touch this. +* Asynchronous functions ONLY: Only asynchronous functions need to use + the Future pattern, e.g. anything with a callback. If you are simply + calling an underlying SDK function that finishes its work and returns + immediately, with no callback, there is no need to use a Future. See + `STYLE_GUIDE.md` for more details on asynchronous operations. + +### Core Classes and Operations (Examples from Auth and Database) + +While each Firebase product has its own specific classes, the following +examples illustrate common API patterns: + +* **`firebase::auth::Auth`**: The main entry point for Firebase + Authentication. + * Used to manage users, sign in/out, etc. + * Successful authentication operations (like + `SignInWithEmailAndPassword()`) return a + `Future`. The `firebase::auth::User` + object can then be obtained from this `AuthResult` (e.g., + `auth_result.result()->user()` after `result()` is confirmed + successful and the pointer is checked). + * Example: `firebase::auth::User* current_user = auth->current_user();` + * Methods for user creation/authentication: + `CreateUserWithEmailAndPassword()`, `SignInWithEmailAndPassword()`, + `SignInWithCredential()`. +* **`firebase::auth::User`**: + * Represents a user account. Data is typically accessed via its methods. + * Methods for profile updates: `UpdateEmail()`, `UpdatePassword()`, + `UpdateUserProfile()`. + * Other operations: `SendEmailVerification()`, `Delete()`. +* **`firebase::database::Database`**: The main entry point for Firebase + Realtime Database. + * Used to get `DatabaseReference` objects to specific locations in the + database. +* **`firebase::database::DatabaseReference`**: + * Represents a reference to a specific location (path) in the Realtime + Database. + * Methods for navigation: `Child()`, `Parent()`, `Root()`. + * Methods for data manipulation: `GetValue()`, `SetValue()`, + `UpdateChildren()`, `RemoveValue()`. + * Methods for listeners: `AddValueListener()`, `AddChildListener()`. +* **`firebase::database::Query`**: + * Used to retrieve data from a Realtime Database location based on + specific criteria. Obtained from a `DatabaseReference`. + * **Filtering**: `OrderByChild()`, `OrderByKey()`, `OrderByValue()`, + `EqualTo()`, `StartAt()`, `EndAt()`. + * **Limiting**: `LimitToFirst()`, `LimitToLast()`. + * Execution: `GetValue()` returns a `Future`. +* **`firebase::database::DataSnapshot`**: Contains data read from a Realtime + Database location (either directly or as a result of a query). Accessed + via `future.result()` or through listeners. + * Methods: `value()` (returns a `firebase::Variant` representing the + data), `children_count()`, `children()`, `key()`, `exists()`. +* **`firebase::Variant`**: A type that can hold various data types like + integers, strings, booleans, vectors (arrays), and maps (objects), + commonly used for reading and writing data with Realtime Database and other + services. +* **Operation-Specific Options**: Some operations might take optional + parameters to control behavior, though not always through a dedicated + "Options" class like Firestore's `SetOptions`. For example, + `User::UpdateUserProfile()` takes a `UserProfile` struct. + +### Listeners for Real-time Updates + +Many Firebase services support real-time data synchronization using listeners. + +* **`firebase::database::ValueListener` / + `firebase::database::ChildListener`**: Implemented by the developer and + registered with a `DatabaseReference`. + * `ValueListener::OnValueChanged(const firebase::database::DataSnapshot& snapshot)` + is called when the data at that location changes. + * `ChildListener` has methods like `OnChildAdded()`, `OnChildChanged()`, + `OnChildRemoved()`. +* **`firebase::auth::AuthStateListener`**: Implemented by the developer and + registered with `firebase::auth::Auth`. + * `AuthStateListener::OnAuthStateChanged(firebase::auth::Auth* auth)` is + called when the user's sign-in state changes. +* **Removing Listeners**: Listeners are typically removed by passing the + listener instance to a corresponding `Remove...Listener()` method (e.g., + `reference->RemoveValueListener(my_listener);`, + `auth->RemoveAuthStateListener(my_auth_listener);`). + +This overview provides a general understanding. Always refer to the specific +header files in `firebase/app/client/cpp/include/firebase/` and +`firebase/product_name/client/cpp/include/firebase/product_name/` for detailed +API documentation. + +# Best Practices + +## Coding Style + +* **Firebase C++ Style Guide**: For specific C++ API design and coding + conventions relevant to this SDK, refer to the + [STYLE_GUIDE.md](STYLE_GUIDE.md). +* **Google C++ Style Guide**: Adhere to the + [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) + as mentioned in `CONTRIBUTING.md`. +* **Formatting**: Use `python3 scripts/format_code.py -git_diff -verbose` to + format your code before committing. +* **Naming Precision for Dynamic Systems**: Function names should precisely + reflect their behavior, especially in systems with dynamic or asynchronous + interactions. For example, a function that processes a list of items should + be named differently from one that operates on a single, specific item + captured asynchronously. Regularly re-evaluate function names as + requirements evolve to maintain clarity. + +## Comments + +* Write clear and concise comments where necessary to explain complex logic or + non-obvious behavior. +* **Avoid overly verbose comments**: Do not state the obvious. The code should + be as self-documenting as possible. +* Follow existing comment styles within the module you are working on. +* "Avoid adding comments next to `#include` directives merely to explain why + the include is necessary; the code usage should make this clear, or it can + be part of a broader comment block if truly non-obvious for a section of + code." +* "Do not include comments that narrate the AI agent's iterative development + process (e.g., 'Removed old logic here', 'Changed variable name from X to + Y because...', 'Attempted Z but it did not work'). Comments should focus on + explaining the current state of the code for future maintainers, not its + development history or the AI's thought process." + +## Error Handling + +* **Check `Future` status and errors**: Always check `future.status()` and + `future.error()` before attempting to use `future.result()`. + * A common success code is `0` (e.g., + `firebase::auth::kAuthErrorNone`, + `firebase::database::kErrorNone`). Other specific error codes are + defined per module (e.g., + `firebase::auth::kAuthErrorUserNotFound`). +* **Callback error parameters**: When using listeners or other callbacks, + always check the provided error code and message before processing the + data. +* Provide meaningful error messages if your code introduces new error + conditions. + +## Resource Management + +* **`firebase::Future`**: `Future` objects manage their own resources. + `Future::Release()` can be called, but it's often handled by RAII when + futures go out of scope. Be mindful of `Future` lifetimes if they are + stored as members or passed around. +* **ListenerRegistrations**: When a listener is no longer needed, call + `ListenerRegistration::Remove()` to detach it. Failure to do so can lead + to memory leaks and unnecessary network/CPU usage. +* **Pointers**: Standard C++ smart pointers (`std::unique_ptr`, + `std::shared_ptr`) should be used where appropriate for managing + dynamically allocated memory. +* **`Future` Lifecycle**: Ensure `Future` objects returned from API calls are + properly managed. While `Future`s handle their own internal memory for the + result, the asynchronous operations they represent need to complete to + reliably free all associated operational resources or to ensure actions + (like writes to a database) are definitely finalized. Abandoning a `Future` + (letting it go out of scope without checking its result, attaching an + `OnCompletion` callback, or explicitly `Wait()`ing for it) can sometimes + lead to operations not completing as expected or resources not being + cleaned up promptly by the underlying services, especially if the `Future` + is the only handle to that operation. Prefer using `OnCompletion` or + otherwise ensuring the `Future` completes its course, particularly for + operations with side effects or those that allocate significant backend + resources. +* **Lifecycle of Queued Callbacks/Blocks**: If blocks or callbacks are queued + to be run upon an asynchronous event (e.g., an App Delegate class being set + or a Future completing), clearly define and document their lifecycle. + Determine if they are one-shot (cleared after first execution) or + persistent (intended to run for multiple or future events). This impacts + how associated data and the blocks themselves are stored and cleared, + preventing memory leaks or unexpected multiple executions. + +## Immutability + +* Be aware that some objects, like `firebase::firestore::Query`, are + immutable. Methods that appear to modify them (e.g., `query.Where(...)`) + actually return a new instance with the modification. + +## Platform-Specific Code + +* Firebase C++ SDK supports multiple platforms (Android, iOS, tvOS, desktop - + Windows, macOS, Linux). +* Platform-specific code is typically organized into subdirectories: + * `android/` (for Java/JNI related code) + * `ios/` (for Objective-C/Swift interoperability code) + * `desktop/` (for Windows, macOS, Linux specific implementations) + * `common/` (for C++ code shared across all platforms) +* Use preprocessor directives (e.g., `#if FIREBASE_PLATFORM_ANDROID`, + `#if FIREBASE_PLATFORM_IOS`) to conditionally compile platform-specific + sections when necessary, but prefer separate implementation files where + possible for better organization. + +## Platform-Specific Considerations + +* **Realtime Database (Desktop)**: The C++ SDK for Realtime Database on + desktop platforms (Windows, macOS, Linux) uses a REST-based + implementation. This means that any queries involving + `Query::OrderByChild()` require corresponding indexes to be defined in your + Firebase project's Realtime Database rules. Without these indexes, queries + may fail or not return expected results. +* **iOS Method Swizzling**: Be aware that some Firebase products on iOS + (e.g., Dynamic Links, Cloud Messaging) use method swizzling to + automatically attach handlers to your `AppDelegate`. While this simplifies + integration, it can occasionally be a factor to consider when debugging app + delegate behavior or integrating with other libraries that also perform + swizzling. + When implementing or interacting with swizzling, especially for App Delegate + methods like `[UIApplication setDelegate:]`: + * Be highly aware that `setDelegate:` can be called multiple times + with different delegate class instances, including proxy classes + from other libraries (e.g., GUL - Google Utilities). Swizzling + logic must be robust against being invoked multiple times for the + same effective method on the same class or on classes in a + hierarchy. An idempotency check (i.e., if the method's current IMP + is already the target swizzled IMP, do nothing more for that + specific swizzle attempt) in any swizzling utility can prevent + issues like recursion. + * When tracking unique App Delegate classes (e.g., for applying hooks + or callbacks via swizzling), consider the class hierarchy. If a + superclass has already been processed, processing a subclass for + the same inherited methods might be redundant or problematic. A + strategy to check if a newly set delegate is a subclass of an + already processed delegate can prevent such issues. + * For code that runs very early in the application lifecycle on + iOS/macOS (e.g., `+load` methods, static initializers involved in + swizzling), prefer using `NSLog` directly over custom logging + frameworks if there's any uncertainty about whether the custom + framework is fully initialized, to avoid crashes during logging + itself. + +## Class and File Structure + +* Follow the existing pattern of internal and common classes within each + Firebase library (e.g., `firestore/src/common`, `firestore/src/main`, + `firestore/src/android`). +* Public headers defining the API are typically in + `src/include/firebase/product_name/`. +* Internal implementation classes often use the `Internal` suffix (e.g., + `DocumentReferenceInternal`). + +# Common Patterns + +## Pimpl (Pointer to Implementation) Idiom + +* Many public API classes (e.g., `firebase::storage::StorageReference`, + `firebase::storage::Metadata`) use the Pimpl idiom. +* They hold a pointer to an internal implementation class (e.g., + `StorageReferenceInternal`, `MetadataInternal`). +* This pattern helps decouple the public interface from its implementation, + reducing compilation dependencies and hiding internal details. +* When working with these classes, you will primarily interact with the public + interface. Modifications to the underlying implementation are done in the + `*Internal` classes. + +A common convention for this Pimpl pattern in the codebase (e.g., as seen in +`firebase::storage::Metadata`) is that the public class owns the raw pointer to +its internal implementation (`internal_`). This requires careful manual memory +management: +* **Creation**: The `internal_` object is typically allocated with `new` in + the public class's constructor(s). For instance, the default constructor + might do `internal_ = new ClassNameInternal(...);`, and a copy constructor + would do `internal_ = new ClassNameInternal(*other.internal_);` for a + deep copy. +* **Deletion**: The `internal_` object is `delete`d in the public class's + destructor (e.g., `delete internal_;`). It's good practice to set + `internal_ = nullptr;` immediately after deletion if the destructor isn't + the absolute last point of usage, or if helper delete functions are used + (as seen with `MetadataInternalCommon::DeleteInternal` which nullifies the + pointer in the public class instance before deleting). +* **Copy Semantics**: + * **Copy Constructor**: If supported, a deep copy is usually performed. A + new `internal_` instance is created, and the contents from the source + object's `internal_` are copied into this new instance. + * **Copy Assignment Operator**: If supported, it must also perform a deep + copy. This typically involves deleting the current `internal_` object, + then creating a new `internal_` instance and copying data from the + source's `internal_` object. Standard self-assignment checks should be + present. +* **Move Semantics**: + * **Move Constructor**: If supported, ownership of the `internal_` pointer + is transferred from the source object to the new object. The source + object's `internal_` pointer is then set to `nullptr` to prevent + double deletion. + * **Move Assignment Operator**: Similar to the move constructor, it + involves deleting the current object's `internal_`, then taking + ownership of the source's `internal_` pointer, and finally setting the + source's `internal_` to `nullptr`. +* **Cleanup Registration (Advanced)**: In some cases, like + `firebase::storage::Metadata`, there might be an additional registration + with a central cleanup mechanism (e.g., via `StorageInternal`). This acts + as a safeguard or part of a larger resource management strategy within the + module, but the fundamental responsibility for creation and deletion in + typical scenarios lies with the Pimpl class itself. + +It's crucial to correctly implement all these aspects (constructors, +destructor, copy/move operators) when dealing with raw pointer Pimpls to +prevent memory leaks, dangling pointers, or double deletions. + +## Namespace Usage + +* Code for each Firebase product is typically within its own namespace under + `firebase`. + * Example: `firebase::firestore`, `firebase::auth`, + `firebase::database`. +* Internal or platform-specific implementation details might be in nested + namespaces like `firebase::firestore::internal` or + `firebase::firestore::android`. + +## Futures for Asynchronous Operations + +* As detailed in the API Surface section, `firebase::Future` is the + standard way all asynchronous operations return their results. +* Familiarize yourself with `Future::status()`, `Future::error()`, + `Future::error_message()`, `Future::result()`, and + `Future::OnCompletion()`. + +## Internal Classes and Helpers + +* Each module often has a set of internal classes (often suffixed with + `Internal` or residing in an `internal` namespace) that manage the core + logic, platform-specific interactions, and communication with the Firebase + backend. +* Utility functions and helper classes are common within the `common/` or + `util/` subdirectories of a product. + +## Singleton Usage (Limited) + +* The primary singleton is `firebase::App::GetInstance()`. +* Service entry points like `firebase::firestore::Firestore::GetInstance()` + also provide singleton-like access to service instances (scoped to an `App` + instance). +* Beyond these entry points, direct creation or use of singletons for core + data objects or utility classes is not a dominant pattern. Dependencies are + generally passed explicitly. + +## Platform Abstraction + +* For operations that differ by platform, there's often a common C++ + interface defined in `common/` or `main/`, with specific implementations + in `android/`, `ios/`, and `desktop/` directories. +* JNI is used extensively in `android/` directories for C++ to Java + communication. +* Objective-C++ (`.mm` files) is used in `ios/` directories for C++ to + Objective-C communication. + +## Listener Pattern + +* The `AddSnapshotListener`/`ListenerRegistration` pattern is common for + features requiring real-time updates (e.g., Firestore, Realtime + Database). + +## Builder/Fluent Interface for Queries + +* Classes like `firebase::firestore::Query` use a fluent interface (method + chaining) to construct complex queries by progressively adding filters, + ordering, and limits. Each call typically returns a new instance of the + query object. + +# Updating This Document + +This document is a living guide. As the Firebase C++ SDK evolves, new patterns +may emerge, or existing practices might change. If you introduce a new common +pattern, significantly alter a build process, or establish a new best practice +during your work, please take a moment to update this `AGENTS.md` file +accordingly. + +Keeping this document current will greatly benefit future AI agents and human +developers working on this repository. + +# Prompting Jules AI + +## Recommended General Prompt Instruction + +When working on this task, please consistently refer to the `AGENTS.md` guide +for all repository-specific conventions, including setup procedures, coding +style, common architectural patterns, and API usage. Pay close attention to the +testing strategies outlined, ensuring your implementation includes +comprehensive integration tests with detailed test cases in your plan. Implement +robust error handling for any new or modified public API methods, following the +patterns for `firebase::Future` error reporting. If the feature involves +platform-specific code, ensure the public API remains consistent across all +platforms, as discussed in `AGENTS.md`. Write clear, maintainable code, +adhering to the commenting guidelines, and if you need to add new third-party +dependencies, document the rationale and update build configurations according +to the established practices. Ensure your changes align with the overall best +practices detailed in `AGENTS.md`. + +## Key Directives for Jules AI + +* **Prioritize `AGENTS.md`**: This document (`AGENTS.md`) contains + repository-specific guidelines. Prioritize this information when making + decisions about coding style, testing procedures, architectural patterns, + and API usage. +* **Adherence to Patterns**: Strive to follow the common patterns and best + practices outlined here. This ensures consistency across the codebase. +* **Clarification for Deviations**: If the existing patterns in `AGENTS.md` do + not seem suitable for a specific task, or if a deviation is necessary, + please explicitly state this in your plan or request clarification before + proceeding with implementation. +* **Updating `AGENTS.md`**: If you introduce a new, broadly applicable + pattern, or if a significant change to the build process or best practices + occurs as part of your task, please include a step in your plan to update + `AGENTS.md` to reflect these changes. +* **Testing**: Remember that integration tests are the primary method of + testing. Ensure new features are accompanied by corresponding integration + tests as described in the 'Testing' section of `AGENTS.md`. +* **Commit Messages**: Follow standard commit message guidelines. A brief + summary line, followed by a more detailed explanation if necessary. +* **Tool Limitations & Path Specificity**: If codebase search tools (like + `grep` or recursive `ls`) are limited or unavailable, and initial attempts + to locate files/modules based on common directory structures are + unsuccessful, explicitly ask for more specific paths rather than assuming a + module doesn't exist or contains no relevant code. + +## GitHub Code Review Comments -- IMPORTANT, JULES READ THIS SECTION + +If the user asks you to look at GitHub code review comments, PR reviews, etc, +that means that you should run the following command: + +`python3 scripts/print_github_reviews.py --branch BRANCH_NAME_THAT_JULES_PUSHED_TO_GITHUB` + +(with the remote branch name that was actually pushed to GitHub substituted, and +if you aren't sure what it is or it returns an error, just ask the user, as they +may have renamed the branch before pushing) and then address the comments that +the output shows. If it succeeds, then subsequent times you run the script on +the same branch, include the `--since` parameter in accordance with the previous +script run's output to ensure you only fetch new comments. diff --git a/Android/firebase_dependencies.gradle b/Android/firebase_dependencies.gradle index ac7691f9a6..c7a19de54b 100644 --- a/Android/firebase_dependencies.gradle +++ b/Android/firebase_dependencies.gradle @@ -20,15 +20,13 @@ def firebaseDependenciesMap = [ 'app_check' : ['com.google.firebase:firebase-appcheck', 'com.google.firebase:firebase-appcheck-debug', 'com.google.firebase:firebase-appcheck-playintegrity'], - 'play_services' : ['com.google.android.gms:play-services-base:18.6.0'], + 'play_services' : ['com.google.android.gms:play-services-base:18.7.0'], 'analytics' : ['com.google.firebase:firebase-analytics'], 'auth' : ['com.google.firebase:firebase-auth'], 'database' : ['com.google.firebase:firebase-database'], 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links'], 'firestore' : ['com.google.firebase:firebase-firestore'], 'functions' : ['com.google.firebase:firebase-functions'], - 'gma' : ['com.google.android.gms:play-services-ads:23.0.0', - 'com.google.android.ump:user-messaging-platform:2.2.0'], 'installations' : ['com.google.firebase:firebase-installations'], 'invites' : ['com.google.firebase:firebase-invites'], // Messaging has an additional local dependency to include. @@ -39,7 +37,8 @@ def firebaseDependenciesMap = [ 'performance' : ['com.google.firebase:firebase-perf'], 'remote_config' : ['com.google.firebase:firebase-config'], 'storage' : ['com.google.firebase:firebase-storage'], - 'testlab' : [] + 'testlab' : [], + 'ump' : ['com.google.android.ump:user-messaging-platform:2.2.0'] ] // A map of library to the gradle resources that they depend upon. @@ -51,9 +50,9 @@ def firebaseResourceDependenciesMap = [ 'auth' : [':auth:auth_resources'], 'database' : [':database:database_resources'], 'firestore' : [':firestore:firestore_resources'], - 'gma' : [':gma:gma_resources'], 'remote_config' : [':remote_config:remote_config_resources'], - 'storage' : [':storage:storage_resources'] + 'storage' : [':storage:storage_resources'], + 'ump' : [':ump:ump_resources'] ] def setResourceDependencies(String subproject) { @@ -92,9 +91,6 @@ class Dependencies { def getFirestore() { libSet.add('firestore') } - def getGma() { - libSet.add('gma') - } def getFunctions() { libSet.add('functions') } @@ -116,6 +112,9 @@ class Dependencies { def getStorage() { libSet.add('storage') } + def getUmp() { + libSet.add('ump') + } } // Extension to handle which Firebase C++ dependencies are being added to the @@ -159,7 +158,7 @@ project.afterEvaluate { // Add the bill-of-materials project.dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') } for (String lib : firebaseCpp.dependencies.libSet) { // Generate and include the proguard file diff --git a/CMakeLists.txt b/CMakeLists.txt index 439ea7d5c7..a5a505120e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,7 @@ option(FIREBASE_INCLUDE_FIRESTORE option(FIREBASE_INCLUDE_FUNCTIONS "Include the Cloud Functions for Firebase library." ${FIREBASE_INCLUDE_LIBRARY_DEFAULT}) -option(FIREBASE_INCLUDE_GMA "Include the GMA library." +option(FIREBASE_INCLUDE_UMP "Include the UMP library." ${FIREBASE_INCLUDE_LIBRARY_DEFAULT}) option(FIREBASE_INCLUDE_INSTALLATIONS "Include the Firebase Installations library." @@ -123,9 +123,9 @@ if(FIREBASE_CPP_BUILD_TESTS OR FIREBASE_CPP_BUILD_STUB_TESTS) endif() if (PLATFORM STREQUAL TVOS OR PLATFORM STREQUAL SIMULATOR_TVOS) - # GMA and FDL are not supported on tvOS. + # UMP and FDL are not supported on tvOS. set(FIREBASE_INCLUDE_DYNAMIC_LINKS OFF) - set(FIREBASE_INCLUDE_GMA OFF) + set(FIREBASE_INCLUDE_UMP OFF) endif() # Occasionally ANDROID is not being set correctly when invoked by gradle, so @@ -154,6 +154,11 @@ if(APPLE) # build output for our Objective-C++ files much too verbose. set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-nullability-completeness") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-nullability-completeness") + if(DESKTOP) + # Mac desktop Firestore build requires -Wno-deprecated-declarations + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") + endif() endif() if(DESKTOP AND NOT MSVC AND NOT APPLE) @@ -627,8 +632,8 @@ endif() if (FIREBASE_INCLUDE_FUNCTIONS) add_subdirectory(functions) endif() -if (FIREBASE_INCLUDE_GMA) - add_subdirectory(gma) +if (FIREBASE_INCLUDE_UMP) + add_subdirectory(ump) endif() if (FIREBASE_INCLUDE_INSTALLATIONS) add_subdirectory(installations) diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000000..a141ee0b66 --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,378 @@ +# C++ API Guidelines + +**WIP** - *please feel free to improve* + +Intended for Firebase APIs, but also applicable to any C++ or Game APIs. + +# Code Style + +Please comply with the +[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) +as much as possible. Refresh your memory of this document before you start :) + +# C++ API Design + +### Don't force any particular usage pattern upon the client. + +C++ is a huge language, with a great variety of ways in which things can be +done, compared to other languages. As a consequence, C++ projects can be very +particular about what features of the language they use or don't use, how they +represent their data, and structure their code. + +An API that forces the use of a feature or structure the client doesn't use +will be very unwelcome. A good API uses only the simplest common denominator +data types and features, and will be useable by all. This can generally be +done with minimal impact on your API’s simplicity, or at least should form +the baseline API. + +Examples of typical Do's and Don'ts: + +* Don't force the use of a particular data structure to supply or receive + data. Typical examples: + * `std::vector`: If the client doesn't have the data already in a + `std::vector` (or only wants to use part of a vector), they are forced + to copy/allocate a new one, and C++ programmers don't like unnecessary + copies/allocations. + Instead, your primary interface should always take a + `(const T *, size_t)` instead. You can still supply an optional helper + method that takes a `std::vector &` and then calls the former if + you anticipate it to be called very frequently. + * `std::string`: Unlike Java, these things aren't pooled, they're mutable + and copied. A common mistake is to take a `const std::string &` + argument, which forces all callers that supply a `const char *` to go + thru a strlen+malloc+copy that is possibly of no use to the callee. + Prefer to take a `const char *` instead for things that are + names/identifiers, especially if they possibly are compile-time + constant. If you're unsetting a string property, prefer to pass + nullptr rather than an empty string. (There are + [COW implementations](https://en.wikipedia.org/wiki/Copy-on-write), + but you can't rely on that). + * `std::map`: This is a costly data structure involving many + allocations. If all you wanted is for the caller to supply a list of + key/value pairs, take a `const char **` (yes, 2 stars!). Or + `const SimpleStruct *` instead, which allows the user to create this + data statically. +* Per-product configuration should be accomplished using an options struct + passed to the library's `firebase::::Initialize` function. + Default options should be provided by the options struct's default + constructor. The `Initialize` function should be overloaded with a version + that does not take the options struct (which is how the Google style guide + prefers that we pass default parameters). + + For example, + +```c++ + struct LibraryOptions { + LibraryOptions() : do_the_thing(false) {} + + bool do_the_thing; + }; + + InitResult Initialize(const App& app) { + return Initialize(app, LibraryOptions()); + } + InitResult Initialize(const App& app, const LibraryOptions& options); +``` + +* Don't make your client responsible for data you allocate or take ownership + of client data. Typical C++ APIs are written such that both the client + and the library own their own memory, and they take full responsibility + for managing it, regardless of what the other does. Any data exchanged is + typically done through weak references and copies. + An exception may be a file loading API where buffers exchanged may be + really big. If you are going to pass ownership, make this super obvious + in all comments and documentation (C++ programmers typically won't + expect it), and document which function should be used to free the data + (free, delete, or a custom one). + Alternatively, a simple way to pass ownership of a large new buffer to + the client is to ask the client to supply a std::string *, which you then + resize(), and write directly into its owned memory. This somewhat + violates the rule about use of std::string above, though. + +* Don't use exceptions. This one is worth mentioning separately. Though + exceptions are great in theory, in C++ hardly any libraries use them, and + many code-bases disallow them entirely. They also require the use of RTTI + which some environments turn off. Oh, yes, also don't use RTTI. + +* Go easy on templates when possible. Yes, they make your code more general, + but they also pull a lot of implementation detail into your API, lengthen + compile times and bloat binaries. In C++ they are glorified macros, so + they result in hard to understand errors, and can make correct use of + your API harder to understand. + +* Utilize C++11 features where appropriate. This project has adopted C++11, + and features such as `std::unique_ptr`, `std::shared_ptr`, + `std::make_unique`, and `std::move` are encouraged to improve code safety + and readability. However, avoid features from C++14 or newer standards. + +* Go easy on objectifying everything, and prefer value types. In languages + like Java it is common to give each "concept" your API deals with its own + class, such that methods on it have a nice home. In C++ this isn't + always desirable, because objects need to be managed, stored and + allocated, and you run into ownership/lifetime questions mentioned above. + Instead: + + * For simple data, prefer their management to happen in the parent + class that owns them. Actions on them are methods in the parent. If + at all possible, prefer not to refer to them by pointer/reference + (which creates ownership and lifetime issues) but by index/id, or + string if not performance sensitive (for example, when referring to + file resources, since the cost of loading a file dwarfs the cost of + a string lookup). + + * If you must create objects, and objects are not heavyweight (only + scalars and non-owned pointers), make use of these objects by value + (return by value, receive by const reference). This makes ownership + and lifetime management trivial and efficient. + +* If at all possible, don't depend on external libraries. C++ compilation, + linking, dependency management, testing (especially cross platform) are + generally way harder than any other language. Every dependency is a + potential source of build complexity, conflicts, efficiency issues, and + in general more dependencies means less adoption. + + * Don't pull in large libraries (e.g. BOOST) just for your + convenience, especially if their use is exposed in headers. + + * Only use external libraries that have hard to replicate essential + functionality (e.g. compression, serialization, image loading, + networking etc.). Make sure to only access them in implementation + files. + + * If possible, make a dependency optional, e.g. if what your API does + benefits from compression, make the client responsible for doing so, + or add an interface for it. Add sample glue code or an optional API + for working with the external library that is by default off in the + build files, and can be switched on if desired. + +* Take cross-platform-ness seriously: design the API to work on ALL + platforms even if you don't intend to supply implementations for all. + Hide platform issues in the implementation. Don't ever include platform + specific headers in your own headers. Have graceful fallback for + platforms you don't support, such that some level of building / testing + can happen anywhere. + +* If your API is meant to be VERY widespread in use, VERY general, and very + simple (e.g. a compression API), consider making at least the API (if + not all of it) in C, as you'll reach an even wider audience. C has a + more consistent ABI and is easier to access from a variety of systems / + languages. This is especially useful if the library implementation is + intended to be provided in binary. + +* Be careful not to to use idioms from other languages that may be foreign + to C++. + + * An example of this is a "Builder" API (common in Java). Prefer to + use regular constructors, with additional set_ methods for less + common parameters if the number of them gets overwhelming. + +* Do not expose your own UI to the user as part of an API. Give the + developer the data to work with, and let them handle displaying it to + the user in the way they see fit. + + * Rare exceptions can be made to this rule on a case-by-case basis. + For example, authentication libraries may need to display a sign-in + UI for the user to enter their credentials. Your API may work with + data owned by Google or by the user (e.g. the user's contacts) that + we don't want to expose to the app; in those cases, it is + appropriate to expose a UI (but to limit the scope of the UI to the + minimum necessary). + + * In these types of exceptional cases, the UI should be in an isolated + component, separate from the rest of the API. We do allow UIs to be + exposed to the user UI-specific libraries, e.g. FirebaseUI, which + should be open-source so developers can apply any customizations + they need. + +# Game API Design + +### Performance matters + +Most of this is already encoded in C++ API design above, but it bears +repeating: C++ game programmers can be more fanatic about performance than +you expect. + +It is easy to add a layer of usability on top of fast code, it is very +hard to impossible to "add performance" to an API that has performance +issues baked into its design. + +### Don't rely on state persisting for longer than one frame. + +Games have an interesting program structure very unlike apps or web pages: +they do all processing (and rendering) of almost all functionality of the +game within a *frame* (usually 1/60th of a second), and then start anew +for the next frame. + +It is common for all or part of the state of a game to be wiped out from +one frame to the next (e.g when going into the menu, loading a new level, +starting a cut-scene..). + +The consequence of this is that the state kept between frames is the only +record of what is currently going on, and that managing this state is a +source of complexity, especially when part of it is reflected in external +code: + +* Prefer API design that is stateless, or if it is stateful, is so only + within a frame (i.e. between the start of the frame and the start of + the next one). This really simplifies the client's use of your API: + they can't forget to "reset" your API's state whenever they change + state themselves. + +* Prefer not to use cross-frame callbacks at all (non-escaping callbacks + are fine). Callbacks can be problematic in other contexts, but they're + even more problematic in games. Since they will execute at a future + time, there's no guarantee that the state that was there when the + callback started will still be there. There's no easy way to robustly + "clear" pending callbacks that don't make sense anymore when state + changes. Instead, make your API based on *polling*. + Yes, everyone learned in school that polling is bad because it uses + CPU, but that's what games are based on anyway: they check a LOT of + things every frame (and only at 60hz, which is very friendly compared + to busy-wait polling). If your API can be in various states of a state + machine (e.g. a networking based API), make sure the client can poll + the state you're in. This can then easily be translated to user + feedback. + If you have to use asynchronous callbacks, see the section on async + operations below. + +* Be robust to the client needing to change state. If work done in your + API involves multiple steps, and the client never gets to the end of + those steps before starting a new sequence, don't be "stuck", but deal + with this gracefully. If the game's state got reset, it will have no + record of what it was doing before. Try to not make the client + responsible for knowing what it was doing. + +* Interaction with threading: + + * If you are going to use threading at all, make sure the use of that + is internal to the library, and any issues of thread-safety don't + leak into the client. Allow what appears to be synchronous access + to a possibly asynchronous implementation. If the asynchronous + nature will be externally visible, see the section on async + operations below. + + * Games are typically hard to thread (since it’s hard to combine with + its per-frame nature), so the client typically should have full + control over it: it is often better to make a fully synchronous + single-threaded library and leave threading it to the client. Do + not try to make your API itself thread-safe, as your API is + unlikely the threading boundary (if your client is threaded, use + of your library is typically isolated to one thread, and they do + not want to pay for synchronization cost they don't use). + + * When you do spin up threads to reduce a workload, it is often a + good idea to do that once per frame, as avoid the above mentioned + state based problems, and while starting threads isn't cheap, you + may find it not a problem to do 60x per second. Alternatively you + can pool them, and make sure you have an explicit way to wait for + their idleness at the end of a frame. + +* Games typically use their own memory allocator (for efficiency, but + also to be able to control and budget usage on memory constrained + systems). For this reason, most game APIs tend to provide allocation + hooks that will be used for all internal allocation. This is even more + important if you wish to be able to transfer ownership of memory. + +* Generally prefer solutions that are low on total memory usage. Games + are always constrained on memory, and having your game be killed by + the OS because the library you use has decided it is efficient to + cache everything is problematic. + + * Prefer to recompute values when possible. + + * When you do cache, give the client control over total memory used + for this purpose. + + * Your memory usage should be predictable and ideally have no peaks. + +# Async Operations + +### Application Initiated Async Operations + +* Use the Future / State Pattern. +* Add a `*LastResult()` method for each async operation method to allow + the caller to poll and not save state. + +e.g. + +```c++ + // Start async operation. + Future SignInWithCredential(...); + // Get the result of the pending / last async operation for the method. + Future SignInWithCredentialLastResult(); + + Usage examples: + // call and register callback + auto& result = SignInWithCredential(); + result.set_callback([](result) { if (result == kComplete) { do_something_neat(); wake_up(); } }); + // wait + + // call and poll #1 (saving result) + auto& result = SignInWithCredential(); + while (result.value() != kComplete) { + // wait + } + + // call and poll #2 (result stored in API) + SignInWithCredential(); + while (SignInWithCredentialLastResult().value() != kComplete) { + } +``` + +### API Initiated Async Event Handling + +* Follow the + [listener / observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) + for API initiated (i.e where the caller doesn't initiate the event) + async events. +* Provide a queued interface to allow users to poll for events. + +e.g. + +```c++ + class GcmListener { + public: + virtual void OnDeletedMessage() {} + virtual void OnMessageReceived(const MessageReceivedData* data) {} + }; + + class GcmListenerQueue : private GcmListener { + public: + enum EventType { + kEventTypeMessageDeleted, + kEventTypeMessageReceived, + }; + + struct Event { + EventType type; + MessageReceivedData data; // Set when type == kEventTypeMessageReceived + }; + + // Returns true when an event is retrieved from the queue. + bool PollEvent(Event *event); + }; + + // Wait for callbacks + class MyListener : public GcmListener { + public: + virtual void OnDeletedMessage() { /* do stuff */ } + virtual void OnMessageReceived() { /* display message */ } + }; + MyListener listener; + gcm::Initialize(app, &listener); + + // Poll + GcmListenerQueue queued_listener; + gcm::Initialize(app, &queued_listener); + GcmListenerQueue::Event event; + while (queued_listener(&event)) { + switch (event.type) { + case kEventTypeMessageDeleted: + // do stuff + break; + case kEventTypeMessageReceived: + // display event.data + break; + } + } +``` diff --git a/analytics/build.gradle b/analytics/build.gradle index 4a5d6149ff..961c866d2c 100644 --- a/analytics/build.gradle +++ b/analytics/build.gradle @@ -86,4 +86,7 @@ apply from: "$rootDir/android_build_files/generate_proguard.gradle" project.afterEvaluate { generateProguardFile('analytics') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/analytics/generate_windows_stubs.py b/analytics/generate_windows_stubs.py new file mode 100755 index 0000000000..fabbf573c4 --- /dev/null +++ b/analytics/generate_windows_stubs.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# 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. + +"""Generate stubs and function pointers for Windows SDK""" + +import argparse +import os +import re +import sys + +HEADER_GUARD_PREFIX = "FIREBASE_ANALYTICS_SRC_WINDOWS_" +INCLUDE_PATH = "src/windows/" +INCLUDE_PREFIX = "analytics/" + INCLUDE_PATH +COPYRIGHT_NOTICE = """// 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. + +""" + +def generate_function_pointers(header_file_path, output_h_path, output_c_path): + """ + Parses a C header file to generate a self-contained header with typedefs, + extern function pointer declarations, and a source file with stub functions, + initialized pointers, and a dynamic loading function for Windows. + + Args: + header_file_path (str): The path to the input C header file. + output_h_path (str): The path for the generated C header output file. + output_c_path (str): The path for the generated C source output file. + """ + print(f"Reading header file: {header_file_path}") + try: + with open(header_file_path, 'r', encoding='utf-8') as f: + header_content = f.read() + except FileNotFoundError: + print(f"Error: Header file not found at '{header_file_path}'") + return + + # --- Extract necessary definitions from the original header --- + + # Find all standard includes (e.g., ) + includes = re.findall(r"#include\s+<.*?>", header_content) + + # Find all typedefs, including their documentation comments + typedefs = re.findall(r"/\*\*(?:[\s\S]*?)\*/\s*typedef[\s\S]*?;\s*", header_content) + + # --- Extract function prototypes --- + function_pattern = re.compile( + r"ANALYTICS_API\s+([\w\s\*]+?)\s+(\w+)\s*\((.*?)\);", + re.DOTALL + ) + matches = function_pattern.finditer(header_content) + + extern_declarations = [] + macro_definitions = [] + stub_functions = [] + pointer_initializations = [] + function_details_for_loader = [] + + for match in matches: + return_type = match.group(1).strip() + function_name = match.group(2).strip() + params_str = match.group(3).strip() + + cleaned_params_for_decl = re.sub(r'\s+', ' ', params_str) if params_str else "" + stub_name = f"Stub_{function_name}" + + # Generate return statement for the stub + if "void" in return_type: + return_statement = " // No return value." + elif "*" in return_type: + return_statement = f' return ({return_type})(&g_stub_memory);' + else: # bool, int64_t, etc. + return_statement = " return 1;" + + stub_function = ( + f"// Stub for {function_name}\n" + f"static {return_type} {stub_name}({params_str}) {{\n" + f"{return_statement}\n" + f"}}" + ) + stub_functions.append(stub_function) + + declaration = f"extern {return_type} (*ptr_{function_name})({cleaned_params_for_decl});" + extern_declarations.append(declaration) + + macro = f'#define {function_name} ptr_{function_name}' + macro_definitions.append(macro) + + pointer_init = f"{return_type} (*ptr_{function_name})({cleaned_params_for_decl}) = &{stub_name};" + pointer_initializations.append(pointer_init) + + function_details_for_loader.append((function_name, return_type, cleaned_params_for_decl)) + + print(f"Found {len(pointer_initializations)} functions. Generating output files...") + + # --- Write the self-contained Header File (.h) --- + header_guard = f"{HEADER_GUARD_PREFIX}{os.path.basename(output_h_path).upper().replace('.', '_')}_" + with open(output_h_path, 'w', encoding='utf-8') as f: + f.write(f"{COPYRIGHT_NOTICE}") + f.write(f"// Generated from {os.path.basename(header_file_path)} by {os.path.basename(sys.argv[0])}\n\n") + f.write(f"#ifndef {header_guard}\n") + f.write(f"#define {header_guard}\n\n") + f.write("#include // needed for bool type in pure C\n\n") + + f.write("// --- Copied from original header ---\n") + f.write("\n".join(includes) + "\n\n") + f.write("".join(typedefs)) + f.write("// --- End of copied section ---\n\n") + + f.write("#ifdef __cplusplus\n") + f.write('extern "C" {\n') + f.write("#endif\n\n") + f.write("// --- Function Pointer Declarations ---\n") + f.write("// clang-format off\n") + f.write("\n".join(extern_declarations)) + f.write("\n\n") + f.write("\n".join(macro_definitions)) + f.write("\n// clang-format on\n") + f.write("\n\n// --- Dynamic Loader Declaration for Windows ---\n") + f.write("#if defined(_WIN32)\n") + f.write('#include // For HMODULE\n') + f.write('// Load Google Analytics functions from the given DLL handle into function pointers.\n') + f.write(f'// Returns the number of functions successfully loaded (out of {len(function_details_for_loader)}).\n') + f.write("int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle);\n\n") + f.write('// Reset all function pointers back to stubs.\n') + f.write("void FirebaseAnalytics_UnloadAnalyticsFunctions(void);\n\n") + f.write("#endif // defined(_WIN32)\n") + f.write("\n#ifdef __cplusplus\n") + f.write("}\n") + f.write("#endif\n\n") + f.write(f"#endif // {header_guard}\n") + + print(f"Successfully generated header file: {output_h_path}") + + # --- Write the Source File (.c) --- + with open(output_c_path, 'w', encoding='utf-8') as f: + f.write(f"{COPYRIGHT_NOTICE}") + f.write(f"// Generated from {os.path.basename(header_file_path)} by {os.path.basename(sys.argv[0])}\n\n") + f.write(f'#include "{INCLUDE_PREFIX}{os.path.basename(output_h_path)}"\n') + f.write('#include \n\n') + f.write("// clang-format off\n\n") + f.write("static void* g_stub_memory = NULL;\n\n") + f.write("// --- Stub Function Definitions ---\n") + f.write("\n\n".join(stub_functions)) + f.write("\n\n\n// --- Function Pointer Initializations ---\n") + f.write("\n".join(pointer_initializations)) + f.write("\n\n// --- Dynamic Loader Function for Windows ---\n") + loader_lines = [ + '#if defined(_WIN32)', + 'int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle) {', + ' int count = 0;\n', + ' if (!dll_handle) {', + ' return count;', + ' }\n' + ] + for name, ret_type, params in function_details_for_loader: + pointer_type_cast = f"({ret_type} (*)({params}))" + proc_check = [ + f' FARPROC proc_{name} = GetProcAddress(dll_handle, "{name}");', + f' if (proc_{name}) {{', + f' ptr_{name} = {pointer_type_cast}proc_{name};', + f' count++;', + f' }}' + ] + loader_lines.extend(proc_check) + loader_lines.append('\n return count;') + loader_lines.append('}\n') + loader_lines.append('void FirebaseAnalytics_UnloadAnalyticsFunctions(void) {') + for name, ret_type, params in function_details_for_loader: + loader_lines.append(f' ptr_{name} = &Stub_{name};'); + loader_lines.append('}\n') + loader_lines.append('#endif // defined(_WIN32)\n') + f.write('\n'.join(loader_lines)) + f.write("// clang-format on\n") + + print(f"Successfully generated C source file: {output_c_path}") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="Generate C stubs and function pointers from a header file." + ) + parser.add_argument( + "--windows_header", + default = os.path.join(os.path.dirname(sys.argv[0]), "windows/include/public/c/analytics.h"), + #required=True, + help="Path to the input C header file." + ) + parser.add_argument( + "--output_header", + default = os.path.join(os.path.dirname(sys.argv[0]), INCLUDE_PATH, "analytics_dynamic.h"), + #required=True, + help="Path for the generated output header file." + ) + parser.add_argument( + "--output_source", + default = os.path.join(os.path.dirname(sys.argv[0]), INCLUDE_PATH, "analytics_dynamic.c"), + #required=True, + help="Path for the generated output source file." + ) + + args = parser.parse_args() + + generate_function_pointers( + args.windows_header, + args.output_header, + args.output_source + ) diff --git a/analytics/integration_test/Podfile b/analytics/integration_test/Podfile index 901af55578..055340b4bc 100644 --- a/analytics/integration_test/Podfile +++ b/analytics/integration_test/Podfile @@ -5,12 +5,12 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Analytics', '11.10.0' + pod 'Firebase/Analytics', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Analytics', '11.10.0' + pod 'Firebase/Analytics', '11.14.0' end post_install do |installer| diff --git a/analytics/integration_test/build.gradle b/analytics/integration_test/build.gradle index 02743d45df..b583820fe8 100644 --- a/analytics/integration_test/build.gradle +++ b/analytics/integration_test/build.gradle @@ -96,3 +96,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/analytics/ios_headers/FIREventNames.h b/analytics/ios_headers/FIREventNames.h index 7635c39e4c..16887f34b8 100644 --- a/analytics/ios_headers/FIREventNames.h +++ b/analytics/ios_headers/FIREventNames.h @@ -1,6 +1,6 @@ // Copyright 2025 Google LLC -// Copied from Firebase Analytics iOS SDK 11.10.0. +// Copied from Firebase Analytics iOS SDK 11.14.0. /// @file FIREventNames.h /// diff --git a/analytics/ios_headers/FIRParameterNames.h b/analytics/ios_headers/FIRParameterNames.h index f0ddc61246..bb799183ec 100644 --- a/analytics/ios_headers/FIRParameterNames.h +++ b/analytics/ios_headers/FIRParameterNames.h @@ -1,6 +1,6 @@ // Copyright 2025 Google LLC -// Copied from Firebase Analytics iOS SDK 11.10.0. +// Copied from Firebase Analytics iOS SDK 11.14.0. /// @file FIRParameterNames.h /// diff --git a/analytics/ios_headers/FIRUserPropertyNames.h b/analytics/ios_headers/FIRUserPropertyNames.h index 54f698591e..f6193dda90 100644 --- a/analytics/ios_headers/FIRUserPropertyNames.h +++ b/analytics/ios_headers/FIRUserPropertyNames.h @@ -1,6 +1,6 @@ // Copyright 2025 Google LLC -// Copied from Firebase Analytics iOS SDK 11.10.0. +// Copied from Firebase Analytics iOS SDK 11.14.0. /// @file FIRUserPropertyNames.h /// diff --git a/analytics/src/windows/analytics_dynamic.c b/analytics/src/windows/analytics_dynamic.c new file mode 100644 index 0000000000..d7f483b72d --- /dev/null +++ b/analytics/src/windows/analytics_dynamic.c @@ -0,0 +1,284 @@ +// 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. + +// Generated from analytics.h by generate_windows_stubs.py + +#include "analytics/src/windows/analytics_dynamic.h" +#include + +// clang-format off + +static void* g_stub_memory = NULL; + +// --- Stub Function Definitions --- +// Stub for GoogleAnalytics_Item_Create +static GoogleAnalytics_Item* Stub_GoogleAnalytics_Item_Create() { + return (GoogleAnalytics_Item*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_Item_InsertInt +static void Stub_GoogleAnalytics_Item_InsertInt(GoogleAnalytics_Item* item, + const char* key, + int64_t value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_InsertDouble +static void Stub_GoogleAnalytics_Item_InsertDouble(GoogleAnalytics_Item* item, + const char* key, + double value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_InsertString +static void Stub_GoogleAnalytics_Item_InsertString(GoogleAnalytics_Item* item, + const char* key, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_Item_Destroy +static void Stub_GoogleAnalytics_Item_Destroy(GoogleAnalytics_Item* item) { + // No return value. +} + +// Stub for GoogleAnalytics_ItemVector_Create +static GoogleAnalytics_ItemVector* Stub_GoogleAnalytics_ItemVector_Create() { + return (GoogleAnalytics_ItemVector*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_ItemVector_InsertItem +static void Stub_GoogleAnalytics_ItemVector_InsertItem(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item) { + // No return value. +} + +// Stub for GoogleAnalytics_ItemVector_Destroy +static void Stub_GoogleAnalytics_ItemVector_Destroy(GoogleAnalytics_ItemVector* item_vector) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_Create +static GoogleAnalytics_EventParameters* Stub_GoogleAnalytics_EventParameters_Create() { + return (GoogleAnalytics_EventParameters*)(&g_stub_memory); +} + +// Stub for GoogleAnalytics_EventParameters_InsertInt +static void Stub_GoogleAnalytics_EventParameters_InsertInt(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + int64_t value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertDouble +static void Stub_GoogleAnalytics_EventParameters_InsertDouble(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + double value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertString +static void Stub_GoogleAnalytics_EventParameters_InsertString(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_InsertItemVector +static void Stub_GoogleAnalytics_EventParameters_InsertItemVector(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + GoogleAnalytics_ItemVector* value) { + // No return value. +} + +// Stub for GoogleAnalytics_EventParameters_Destroy +static void Stub_GoogleAnalytics_EventParameters_Destroy(GoogleAnalytics_EventParameters* event_parameter_map) { + // No return value. +} + +// Stub for GoogleAnalytics_LogEvent +static void Stub_GoogleAnalytics_LogEvent(const char* name, GoogleAnalytics_EventParameters* parameters) { + // No return value. +} + +// Stub for GoogleAnalytics_SetUserProperty +static void Stub_GoogleAnalytics_SetUserProperty(const char* name, + const char* value) { + // No return value. +} + +// Stub for GoogleAnalytics_SetUserId +static void Stub_GoogleAnalytics_SetUserId(const char* user_id) { + // No return value. +} + +// Stub for GoogleAnalytics_ResetAnalyticsData +static void Stub_GoogleAnalytics_ResetAnalyticsData() { + // No return value. +} + +// Stub for GoogleAnalytics_SetAnalyticsCollectionEnabled +static void Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled(bool enabled) { + // No return value. +} + + +// --- Function Pointer Initializations --- +GoogleAnalytics_Item* (*ptr_GoogleAnalytics_Item_Create)() = &Stub_GoogleAnalytics_Item_Create; +void (*ptr_GoogleAnalytics_Item_InsertInt)(GoogleAnalytics_Item* item, const char* key, int64_t value) = &Stub_GoogleAnalytics_Item_InsertInt; +void (*ptr_GoogleAnalytics_Item_InsertDouble)(GoogleAnalytics_Item* item, const char* key, double value) = &Stub_GoogleAnalytics_Item_InsertDouble; +void (*ptr_GoogleAnalytics_Item_InsertString)(GoogleAnalytics_Item* item, const char* key, const char* value) = &Stub_GoogleAnalytics_Item_InsertString; +void (*ptr_GoogleAnalytics_Item_Destroy)(GoogleAnalytics_Item* item) = &Stub_GoogleAnalytics_Item_Destroy; +GoogleAnalytics_ItemVector* (*ptr_GoogleAnalytics_ItemVector_Create)() = &Stub_GoogleAnalytics_ItemVector_Create; +void (*ptr_GoogleAnalytics_ItemVector_InsertItem)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item) = &Stub_GoogleAnalytics_ItemVector_InsertItem; +void (*ptr_GoogleAnalytics_ItemVector_Destroy)(GoogleAnalytics_ItemVector* item_vector) = &Stub_GoogleAnalytics_ItemVector_Destroy; +GoogleAnalytics_EventParameters* (*ptr_GoogleAnalytics_EventParameters_Create)() = &Stub_GoogleAnalytics_EventParameters_Create; +void (*ptr_GoogleAnalytics_EventParameters_InsertInt)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value) = &Stub_GoogleAnalytics_EventParameters_InsertInt; +void (*ptr_GoogleAnalytics_EventParameters_InsertDouble)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value) = &Stub_GoogleAnalytics_EventParameters_InsertDouble; +void (*ptr_GoogleAnalytics_EventParameters_InsertString)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value) = &Stub_GoogleAnalytics_EventParameters_InsertString; +void (*ptr_GoogleAnalytics_EventParameters_InsertItemVector)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value) = &Stub_GoogleAnalytics_EventParameters_InsertItemVector; +void (*ptr_GoogleAnalytics_EventParameters_Destroy)(GoogleAnalytics_EventParameters* event_parameter_map) = &Stub_GoogleAnalytics_EventParameters_Destroy; +void (*ptr_GoogleAnalytics_LogEvent)(const char* name, GoogleAnalytics_EventParameters* parameters) = &Stub_GoogleAnalytics_LogEvent; +void (*ptr_GoogleAnalytics_SetUserProperty)(const char* name, const char* value) = &Stub_GoogleAnalytics_SetUserProperty; +void (*ptr_GoogleAnalytics_SetUserId)(const char* user_id) = &Stub_GoogleAnalytics_SetUserId; +void (*ptr_GoogleAnalytics_ResetAnalyticsData)() = &Stub_GoogleAnalytics_ResetAnalyticsData; +void (*ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled)(bool enabled) = &Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled; + +// --- Dynamic Loader Function for Windows --- +#if defined(_WIN32) +int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle) { + int count = 0; + + if (!dll_handle) { + return count; + } + + FARPROC proc_GoogleAnalytics_Item_Create = GetProcAddress(dll_handle, "GoogleAnalytics_Item_Create"); + if (proc_GoogleAnalytics_Item_Create) { + ptr_GoogleAnalytics_Item_Create = (GoogleAnalytics_Item* (*)())proc_GoogleAnalytics_Item_Create; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertInt = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertInt"); + if (proc_GoogleAnalytics_Item_InsertInt) { + ptr_GoogleAnalytics_Item_InsertInt = (void (*)(GoogleAnalytics_Item* item, const char* key, int64_t value))proc_GoogleAnalytics_Item_InsertInt; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertDouble = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertDouble"); + if (proc_GoogleAnalytics_Item_InsertDouble) { + ptr_GoogleAnalytics_Item_InsertDouble = (void (*)(GoogleAnalytics_Item* item, const char* key, double value))proc_GoogleAnalytics_Item_InsertDouble; + count++; + } + FARPROC proc_GoogleAnalytics_Item_InsertString = GetProcAddress(dll_handle, "GoogleAnalytics_Item_InsertString"); + if (proc_GoogleAnalytics_Item_InsertString) { + ptr_GoogleAnalytics_Item_InsertString = (void (*)(GoogleAnalytics_Item* item, const char* key, const char* value))proc_GoogleAnalytics_Item_InsertString; + count++; + } + FARPROC proc_GoogleAnalytics_Item_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_Item_Destroy"); + if (proc_GoogleAnalytics_Item_Destroy) { + ptr_GoogleAnalytics_Item_Destroy = (void (*)(GoogleAnalytics_Item* item))proc_GoogleAnalytics_Item_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_Create = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_Create"); + if (proc_GoogleAnalytics_ItemVector_Create) { + ptr_GoogleAnalytics_ItemVector_Create = (GoogleAnalytics_ItemVector* (*)())proc_GoogleAnalytics_ItemVector_Create; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_InsertItem = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_InsertItem"); + if (proc_GoogleAnalytics_ItemVector_InsertItem) { + ptr_GoogleAnalytics_ItemVector_InsertItem = (void (*)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item))proc_GoogleAnalytics_ItemVector_InsertItem; + count++; + } + FARPROC proc_GoogleAnalytics_ItemVector_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_ItemVector_Destroy"); + if (proc_GoogleAnalytics_ItemVector_Destroy) { + ptr_GoogleAnalytics_ItemVector_Destroy = (void (*)(GoogleAnalytics_ItemVector* item_vector))proc_GoogleAnalytics_ItemVector_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_Create = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_Create"); + if (proc_GoogleAnalytics_EventParameters_Create) { + ptr_GoogleAnalytics_EventParameters_Create = (GoogleAnalytics_EventParameters* (*)())proc_GoogleAnalytics_EventParameters_Create; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertInt = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertInt"); + if (proc_GoogleAnalytics_EventParameters_InsertInt) { + ptr_GoogleAnalytics_EventParameters_InsertInt = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value))proc_GoogleAnalytics_EventParameters_InsertInt; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertDouble = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertDouble"); + if (proc_GoogleAnalytics_EventParameters_InsertDouble) { + ptr_GoogleAnalytics_EventParameters_InsertDouble = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value))proc_GoogleAnalytics_EventParameters_InsertDouble; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertString = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertString"); + if (proc_GoogleAnalytics_EventParameters_InsertString) { + ptr_GoogleAnalytics_EventParameters_InsertString = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value))proc_GoogleAnalytics_EventParameters_InsertString; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_InsertItemVector = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_InsertItemVector"); + if (proc_GoogleAnalytics_EventParameters_InsertItemVector) { + ptr_GoogleAnalytics_EventParameters_InsertItemVector = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value))proc_GoogleAnalytics_EventParameters_InsertItemVector; + count++; + } + FARPROC proc_GoogleAnalytics_EventParameters_Destroy = GetProcAddress(dll_handle, "GoogleAnalytics_EventParameters_Destroy"); + if (proc_GoogleAnalytics_EventParameters_Destroy) { + ptr_GoogleAnalytics_EventParameters_Destroy = (void (*)(GoogleAnalytics_EventParameters* event_parameter_map))proc_GoogleAnalytics_EventParameters_Destroy; + count++; + } + FARPROC proc_GoogleAnalytics_LogEvent = GetProcAddress(dll_handle, "GoogleAnalytics_LogEvent"); + if (proc_GoogleAnalytics_LogEvent) { + ptr_GoogleAnalytics_LogEvent = (void (*)(const char* name, GoogleAnalytics_EventParameters* parameters))proc_GoogleAnalytics_LogEvent; + count++; + } + FARPROC proc_GoogleAnalytics_SetUserProperty = GetProcAddress(dll_handle, "GoogleAnalytics_SetUserProperty"); + if (proc_GoogleAnalytics_SetUserProperty) { + ptr_GoogleAnalytics_SetUserProperty = (void (*)(const char* name, const char* value))proc_GoogleAnalytics_SetUserProperty; + count++; + } + FARPROC proc_GoogleAnalytics_SetUserId = GetProcAddress(dll_handle, "GoogleAnalytics_SetUserId"); + if (proc_GoogleAnalytics_SetUserId) { + ptr_GoogleAnalytics_SetUserId = (void (*)(const char* user_id))proc_GoogleAnalytics_SetUserId; + count++; + } + FARPROC proc_GoogleAnalytics_ResetAnalyticsData = GetProcAddress(dll_handle, "GoogleAnalytics_ResetAnalyticsData"); + if (proc_GoogleAnalytics_ResetAnalyticsData) { + ptr_GoogleAnalytics_ResetAnalyticsData = (void (*)())proc_GoogleAnalytics_ResetAnalyticsData; + count++; + } + FARPROC proc_GoogleAnalytics_SetAnalyticsCollectionEnabled = GetProcAddress(dll_handle, "GoogleAnalytics_SetAnalyticsCollectionEnabled"); + if (proc_GoogleAnalytics_SetAnalyticsCollectionEnabled) { + ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled = (void (*)(bool enabled))proc_GoogleAnalytics_SetAnalyticsCollectionEnabled; + count++; + } + + return count; +} + +void FirebaseAnalytics_UnloadAnalyticsFunctions(void) { + ptr_GoogleAnalytics_Item_Create = &Stub_GoogleAnalytics_Item_Create; + ptr_GoogleAnalytics_Item_InsertInt = &Stub_GoogleAnalytics_Item_InsertInt; + ptr_GoogleAnalytics_Item_InsertDouble = &Stub_GoogleAnalytics_Item_InsertDouble; + ptr_GoogleAnalytics_Item_InsertString = &Stub_GoogleAnalytics_Item_InsertString; + ptr_GoogleAnalytics_Item_Destroy = &Stub_GoogleAnalytics_Item_Destroy; + ptr_GoogleAnalytics_ItemVector_Create = &Stub_GoogleAnalytics_ItemVector_Create; + ptr_GoogleAnalytics_ItemVector_InsertItem = &Stub_GoogleAnalytics_ItemVector_InsertItem; + ptr_GoogleAnalytics_ItemVector_Destroy = &Stub_GoogleAnalytics_ItemVector_Destroy; + ptr_GoogleAnalytics_EventParameters_Create = &Stub_GoogleAnalytics_EventParameters_Create; + ptr_GoogleAnalytics_EventParameters_InsertInt = &Stub_GoogleAnalytics_EventParameters_InsertInt; + ptr_GoogleAnalytics_EventParameters_InsertDouble = &Stub_GoogleAnalytics_EventParameters_InsertDouble; + ptr_GoogleAnalytics_EventParameters_InsertString = &Stub_GoogleAnalytics_EventParameters_InsertString; + ptr_GoogleAnalytics_EventParameters_InsertItemVector = &Stub_GoogleAnalytics_EventParameters_InsertItemVector; + ptr_GoogleAnalytics_EventParameters_Destroy = &Stub_GoogleAnalytics_EventParameters_Destroy; + ptr_GoogleAnalytics_LogEvent = &Stub_GoogleAnalytics_LogEvent; + ptr_GoogleAnalytics_SetUserProperty = &Stub_GoogleAnalytics_SetUserProperty; + ptr_GoogleAnalytics_SetUserId = &Stub_GoogleAnalytics_SetUserId; + ptr_GoogleAnalytics_ResetAnalyticsData = &Stub_GoogleAnalytics_ResetAnalyticsData; + ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled = &Stub_GoogleAnalytics_SetAnalyticsCollectionEnabled; +} + +#endif // defined(_WIN32) +// clang-format on diff --git a/analytics/src/windows/analytics_dynamic.h b/analytics/src/windows/analytics_dynamic.h new file mode 100644 index 0000000000..a35784e980 --- /dev/null +++ b/analytics/src/windows/analytics_dynamic.h @@ -0,0 +1,132 @@ +// 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. + +// Generated from analytics.h by generate_windows_stubs.py + +#ifndef FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ +#define FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ + +#include // needed for bool type in pure C + +// --- Copied from original header --- +#include + +/** + * @brief Opaque type for an item. + * + * This type is an opaque object that represents an item in an item vector. + * + * The caller is responsible for creating the item using the + * GoogleAnalytics_Item_Create() function, and destroying it using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_Item_Opaque GoogleAnalytics_Item; + +/** + * @brief Opaque type for an item vector. + * + * This type is an opaque object that represents a list of items. It is + * used to pass item vectors to the + * GoogleAnalytics_EventParameters_InsertItemVector() function. + * + * The caller is responsible for creating the item vector using the + * GoogleAnalytics_ItemVector_Create() function, and destroying it using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_ItemVector_Opaque GoogleAnalytics_ItemVector; + +/** + * @brief Opaque type for an event parameter map. + * + * This type is an opaque object that represents a dictionary of event + * parameters. It is used to pass event parameters to the + * GoogleAnalytics_LogEvent() function. + * + * The caller is responsible for creating the event parameter map using the + * GoogleAnalytics_EventParameters_Create() function, and destroying it using + * the GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically. + */ +typedef struct GoogleAnalytics_EventParameters_Opaque + GoogleAnalytics_EventParameters; + +// --- End of copied section --- + +#ifdef __cplusplus +extern "C" { +#endif + +// --- Function Pointer Declarations --- +// clang-format off +extern GoogleAnalytics_Item* (*ptr_GoogleAnalytics_Item_Create)(); +extern void (*ptr_GoogleAnalytics_Item_InsertInt)(GoogleAnalytics_Item* item, const char* key, int64_t value); +extern void (*ptr_GoogleAnalytics_Item_InsertDouble)(GoogleAnalytics_Item* item, const char* key, double value); +extern void (*ptr_GoogleAnalytics_Item_InsertString)(GoogleAnalytics_Item* item, const char* key, const char* value); +extern void (*ptr_GoogleAnalytics_Item_Destroy)(GoogleAnalytics_Item* item); +extern GoogleAnalytics_ItemVector* (*ptr_GoogleAnalytics_ItemVector_Create)(); +extern void (*ptr_GoogleAnalytics_ItemVector_InsertItem)(GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item); +extern void (*ptr_GoogleAnalytics_ItemVector_Destroy)(GoogleAnalytics_ItemVector* item_vector); +extern GoogleAnalytics_EventParameters* (*ptr_GoogleAnalytics_EventParameters_Create)(); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertInt)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, int64_t value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertDouble)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, double value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertString)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, const char* value); +extern void (*ptr_GoogleAnalytics_EventParameters_InsertItemVector)(GoogleAnalytics_EventParameters* event_parameter_map, const char* key, GoogleAnalytics_ItemVector* value); +extern void (*ptr_GoogleAnalytics_EventParameters_Destroy)(GoogleAnalytics_EventParameters* event_parameter_map); +extern void (*ptr_GoogleAnalytics_LogEvent)(const char* name, GoogleAnalytics_EventParameters* parameters); +extern void (*ptr_GoogleAnalytics_SetUserProperty)(const char* name, const char* value); +extern void (*ptr_GoogleAnalytics_SetUserId)(const char* user_id); +extern void (*ptr_GoogleAnalytics_ResetAnalyticsData)(); +extern void (*ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled)(bool enabled); + +#define GoogleAnalytics_Item_Create ptr_GoogleAnalytics_Item_Create +#define GoogleAnalytics_Item_InsertInt ptr_GoogleAnalytics_Item_InsertInt +#define GoogleAnalytics_Item_InsertDouble ptr_GoogleAnalytics_Item_InsertDouble +#define GoogleAnalytics_Item_InsertString ptr_GoogleAnalytics_Item_InsertString +#define GoogleAnalytics_Item_Destroy ptr_GoogleAnalytics_Item_Destroy +#define GoogleAnalytics_ItemVector_Create ptr_GoogleAnalytics_ItemVector_Create +#define GoogleAnalytics_ItemVector_InsertItem ptr_GoogleAnalytics_ItemVector_InsertItem +#define GoogleAnalytics_ItemVector_Destroy ptr_GoogleAnalytics_ItemVector_Destroy +#define GoogleAnalytics_EventParameters_Create ptr_GoogleAnalytics_EventParameters_Create +#define GoogleAnalytics_EventParameters_InsertInt ptr_GoogleAnalytics_EventParameters_InsertInt +#define GoogleAnalytics_EventParameters_InsertDouble ptr_GoogleAnalytics_EventParameters_InsertDouble +#define GoogleAnalytics_EventParameters_InsertString ptr_GoogleAnalytics_EventParameters_InsertString +#define GoogleAnalytics_EventParameters_InsertItemVector ptr_GoogleAnalytics_EventParameters_InsertItemVector +#define GoogleAnalytics_EventParameters_Destroy ptr_GoogleAnalytics_EventParameters_Destroy +#define GoogleAnalytics_LogEvent ptr_GoogleAnalytics_LogEvent +#define GoogleAnalytics_SetUserProperty ptr_GoogleAnalytics_SetUserProperty +#define GoogleAnalytics_SetUserId ptr_GoogleAnalytics_SetUserId +#define GoogleAnalytics_ResetAnalyticsData ptr_GoogleAnalytics_ResetAnalyticsData +#define GoogleAnalytics_SetAnalyticsCollectionEnabled ptr_GoogleAnalytics_SetAnalyticsCollectionEnabled +// clang-format on + + +// --- Dynamic Loader Declaration for Windows --- +#if defined(_WIN32) +#include // For HMODULE +// Load Google Analytics functions from the given DLL handle into function pointers. +// Returns the number of functions successfully loaded (out of 19). +int FirebaseAnalytics_LoadAnalyticsFunctions(HMODULE dll_handle); + +// Reset all function pointers back to stubs. +void FirebaseAnalytics_UnloadAnalyticsFunctions(void); + +#endif // defined(_WIN32) + +#ifdef __cplusplus +} +#endif + +#endif // FIREBASE_ANALYTICS_SRC_WINDOWS_ANALYTICS_DYNAMIC_H_ diff --git a/analytics/windows/analytics_win.dll b/analytics/windows/analytics_win.dll new file mode 100755 index 0000000000..f0c83825e3 Binary files /dev/null and b/analytics/windows/analytics_win.dll differ diff --git a/analytics/windows/include/public/analytics.h b/analytics/windows/include/public/analytics.h new file mode 100644 index 0000000000..d2dcc448ae --- /dev/null +++ b/analytics/windows/include/public/analytics.h @@ -0,0 +1,225 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ + +#include +#include +#include +#include +#include +#include + +#include "c/analytics.h" + +namespace google::analytics { + +/** + * The top level Firebase Analytics singleton that provides methods for logging + * events and setting user properties. See the + * developer guides for general information on using Firebase Analytics in + * your apps. + * + * @note The Analytics SDK uses SQLite to persist events and other app-specific + * data. Calling certain thread-unsafe global SQLite methods like + * `sqlite3_shutdown()` can result in unexpected crashes at runtime. + */ +class Analytics { + public: + using PrimitiveValue = std::variant; + using Item = std::unordered_map; + using ItemVector = std::vector; + using EventParameterValue = + std::variant; + using EventParameters = std::unordered_map; + + /** + * @brief Returns the singleton instance of the Analytics class. + */ + static Analytics& GetInstance() { + static Analytics instance; + return instance; + } + + // This type is neither copyable nor movable. + Analytics(const Analytics&) = delete; + Analytics& operator=(const Analytics&) = delete; + Analytics(Analytics&&) = delete; + Analytics& operator=(Analytics&&) = delete; + + /** + * @brief Logs an app event. + * + * The event can have up to 25 parameters. Events with the same name must have + * the same parameters. Up to 500 event names are supported. Using predefined + * events and/or parameters is recommended for optimal reporting. + * + * The following event names are reserved and cannot be used: + * - ad_activeview + * - ad_click + * - ad_exposure + * - ad_query + * - ad_reward + * - adunit_exposure + * - app_clear_data + * - app_exception + * - app_remove + * - app_store_refund + * - app_store_subscription_cancel + * - app_store_subscription_convert + * - app_store_subscription_renew + * - app_update + * - app_upgrade + * - dynamic_link_app_open + * - dynamic_link_app_update + * - dynamic_link_first_open + * - error + * - firebase_campaign + * - first_open + * - first_visit + * - in_app_purchase + * - notification_dismiss + * - notification_foreground + * - notification_open + * - notification_receive + * - os_update + * - session_start + * - session_start_with_rollout + * - user_engagement + * + * @param[in] name The name of the event. Should contain 1 to 40 alphanumeric + * characters or underscores. The name must start with an alphabetic + * character. Some event names are reserved. See event_names.h for the list + * of reserved event names. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used. Note that event names are case-sensitive + * and that logging two events whose names differ only in case will result in + * two distinct events. To manually log screen view events, use the + * `screen_view` event name. Must be UTF-8 encoded. + * @param[in] parameters The map of event parameters. Passing `std::nullopt` + * indicates that the event has no parameters. Parameter names can be up to 40 + * characters long and must start with an alphabetic character and contain + * only alphanumeric characters and underscores. Only String, Int, and Double + * parameter types are supported. String parameter values can be up to 100 + * characters long for standard Google Analytics properties, and up to 500 + * characters long for Google Analytics 360 properties. The "firebase_", + * "google_", and "ga_" prefixes are reserved and should not be used for + * parameter names. String keys and values must be UTF-8 encoded. + */ + void LogEvent(const std::string& event_name, + const std::optional& parameters) { + if (!parameters.has_value()) { + GoogleAnalytics_LogEvent(std::string(event_name).c_str(), nullptr); + return; + } + GoogleAnalytics_EventParameters* map = + GoogleAnalytics_EventParameters_Create(); + for (const auto& [name, value] : parameters.value()) { + if (auto* int_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertInt(map, name.c_str(), + *int_value); + } else if (auto* double_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertDouble(map, name.c_str(), + *double_value); + } else if (auto* string_value = std::get_if(&value)) { + GoogleAnalytics_EventParameters_InsertString(map, name.c_str(), + string_value->c_str()); + } else if (auto* items = std::get_if(&value)) { + GoogleAnalytics_ItemVector* item_vector = + GoogleAnalytics_ItemVector_Create(); + for (const auto& item : *items) { + GoogleAnalytics_Item* nested_item = GoogleAnalytics_Item_Create(); + for (const auto& [nested_name, nested_value] : item) { + if (auto* nested_int_value = std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertInt(nested_item, nested_name.c_str(), + *nested_int_value); + } else if (auto* nested_double_value = + std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertDouble( + nested_item, nested_name.c_str(), *nested_double_value); + } else if (auto* nested_string_value = + std::get_if(&nested_value)) { + GoogleAnalytics_Item_InsertString(nested_item, + nested_name.c_str(), + nested_string_value->c_str()); + } + } + GoogleAnalytics_ItemVector_InsertItem(item_vector, nested_item); + } + GoogleAnalytics_EventParameters_InsertItemVector(map, name.c_str(), + item_vector); + } + } + GoogleAnalytics_LogEvent(std::string(event_name).c_str(), map); + } + + /** + * @brief Sets a user property to a given value. + * + * Up to 25 user property names are supported. Once set, user property values + * persist throughout the app lifecycle and across sessions. + * + * The following user property names are reserved and cannot be used: + * + * - first_open_time + * - last_deep_link_referrer + * - user_id + * + * @param[in] name The name of the user property to set. Should contain 1 to + * 24 alphanumeric characters or underscores, and must start with an + * alphabetic character. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used for user property names. Must be UTF-8 + * encoded. + * @param[in] value The value of the user property. Values can be up to 36 + * characters long. Setting the value to `std::nullopt` removes the user + * property. Must be UTF-8 encoded. + */ + void SetUserProperty(const std::string& name, + std::optional value) { + const char* value_ptr = value.has_value() ? value->c_str() : nullptr; + GoogleAnalytics_SetUserProperty(name.c_str(), value_ptr); + } + + /** + * @brief Sets the user ID property. + * + * This feature must be used in accordance with + * Google's Privacy + * Policy + * + * @param[in] user_id The user ID associated with the user of this app on this + * device. The user ID must be non-empty and no more than 256 characters + * long, and UTF-8 encoded. Setting user_id to std::nullopt removes the + * user ID. + */ + void SetUserId(std::optional user_id) { + if (!user_id.has_value()) { + GoogleAnalytics_SetUserId(nullptr); + } else { + GoogleAnalytics_SetUserId(user_id->c_str()); + } + } + + /** + * @brief Clears all analytics data for this instance from the device and + * resets the app instance ID. + */ + void ResetAnalyticsData() { GoogleAnalytics_ResetAnalyticsData(); } + + /** + * @brief Sets whether analytics collection is enabled for this app on this + * device. + * + * This setting is persisted across app sessions. By default it is enabled. + * + * @param[in] enabled A flag that enables or disables Analytics collection. + */ + void SetAnalyticsCollectionEnabled(bool enabled) { + GoogleAnalytics_SetAnalyticsCollectionEnabled(enabled); + } + + private: + Analytics() = default; +}; + +} // namespace google::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_ANALYTICS_H_ diff --git a/analytics/windows/include/public/c/analytics.h b/analytics/windows/include/public/c/analytics.h new file mode 100644 index 0000000000..cb3047f310 --- /dev/null +++ b/analytics/windows/include/public/c/analytics.h @@ -0,0 +1,332 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ + +#include + +#if defined(ANALYTICS_DLL) && defined(_WIN32) +#define ANALYTICS_API __declspec(dllexport) +#else +#define ANALYTICS_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque type for an item. + * + * This type is an opaque object that represents an item in an item vector. + * + * The caller is responsible for creating the item using the + * GoogleAnalytics_Item_Create() function, and destroying it using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_Item_Opaque GoogleAnalytics_Item; + +/** + * @brief Opaque type for an item vector. + * + * This type is an opaque object that represents a list of items. It is + * used to pass item vectors to the + * GoogleAnalytics_EventParameters_InsertItemVector() function. + * + * The caller is responsible for creating the item vector using the + * GoogleAnalytics_ItemVector_Create() function, and destroying it using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed at that time. + */ +typedef struct GoogleAnalytics_ItemVector_Opaque GoogleAnalytics_ItemVector; + +/** + * @brief Opaque type for an event parameter map. + * + * This type is an opaque object that represents a dictionary of event + * parameters. It is used to pass event parameters to the + * GoogleAnalytics_LogEvent() function. + * + * The caller is responsible for creating the event parameter map using the + * GoogleAnalytics_EventParameters_Create() function, and destroying it using + * the GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically. + */ +typedef struct GoogleAnalytics_EventParameters_Opaque + GoogleAnalytics_EventParameters; + +/** + * @brief Creates an item. + * + * The caller is responsible for destroying the item using the + * GoogleAnalytics_Item_Destroy() function, unless it has been added to an + * item vector, in which case it will be destroyed when it is added. + */ +ANALYTICS_API GoogleAnalytics_Item* GoogleAnalytics_Item_Create(); + +/** + * @brief Inserts an int parameter into the item. + * + * @param[in] item The item to insert the int parameter into. + * @param[in] key The key of the int parameter. Must be UTF-8 encoded. + * @param[in] value The value of the int parameter. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertInt(GoogleAnalytics_Item* item, + const char* key, + int64_t value); + +/** + * @brief Inserts a double parameter into the item. + * + * @param[in] item The item to insert the double parameter into. + * @param[in] key The key of the double parameter. Must be UTF-8 encoded. + * @param[in] value The value of the double parameter. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertDouble(GoogleAnalytics_Item* item, + const char* key, + double value); + +/** + * @brief Inserts a string parameter into the item. + * + * @param[in] item The item to insert the string parameter into. + * @param[in] key The key of the string parameter. Must be UTF-8 encoded. + * @param[in] value The value of the string parameter. Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_Item_InsertString(GoogleAnalytics_Item* item, + const char* key, + const char* value); + +/** + * @brief Destroys the item. + * + * The caller is responsible for destroying the item using this + * function, unless it has been added to an item vector, in which case it + * will be destroyed when it is added. + * + * @param[in] item The item to destroy. + */ +ANALYTICS_API void GoogleAnalytics_Item_Destroy(GoogleAnalytics_Item* item); + +/** + * @brief Creates an item vector. + * + * The caller is responsible for destroying the item vector using the + * GoogleAnalytics_ItemVector_Destroy() function, unless it has been added + * to an event parameter map, in which case it will be destroyed when it is + * added. + */ +ANALYTICS_API GoogleAnalytics_ItemVector* GoogleAnalytics_ItemVector_Create(); + +/** + * @brief Inserts a item into the item vector. + * + * @param[in] item_vector The item vector to insert the item into. + * @param[in] item The item to insert. Automatically destroyed when added. + */ +ANALYTICS_API void GoogleAnalytics_ItemVector_InsertItem( + GoogleAnalytics_ItemVector* item_vector, GoogleAnalytics_Item* item); + +/** + * @brief Destroys the item vector. + * + * The caller has the option to destroy the item vector using this function, + * unless it has been added to an event parameter map, in which case it will be + * destroyed when it is added. + * + * @param[in] item_vector The item vector to destroy. + */ +ANALYTICS_API void GoogleAnalytics_ItemVector_Destroy( + GoogleAnalytics_ItemVector* item_vector); + +/** + * @brief Creates an event parameter map. + * + * The caller is responsible for destroying the event parameter map using the + * GoogleAnalytics_EventParameters_Destroy() function, unless it has been + * logged, in which case it will be destroyed automatically when it is logged. + */ +ANALYTICS_API GoogleAnalytics_EventParameters* +GoogleAnalytics_EventParameters_Create(); + +/** + * @brief Inserts an int parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the int + * parameter into. + * @param[in] key The key of the int parameter. Must be UTF-8 encoded. + * @param[in] value The value of the int parameter. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertInt( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + int64_t value); + +/** + * @brief Inserts a double parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the double + * parameter into. + * @param[in] key The key of the double parameter. Must be UTF-8 encoded. + * @param[in] value The value of the double parameter. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertDouble( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + double value); + +/** + * @brief Inserts a string parameter into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the string + * parameter into. + * @param[in] key The key of the string parameter. Must be UTF-8 encoded. + * @param[in] value The value of the string parameter. Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertString( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + const char* value); + +/** + * @brief Inserts an item vector into the event parameter map. + * + * @param[in] event_parameter_map The event parameter map to insert the item + * vector into. + * @param[in] key The key of the item vector. Must be UTF-8 encoded. + * @param[in] value The value of the item vector. Automatically destroyed as it + * is added. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_InsertItemVector( + GoogleAnalytics_EventParameters* event_parameter_map, const char* key, + GoogleAnalytics_ItemVector* value); + +/** + * @brief Destroys the event parameter map. + * + * The caller is responsible for destroying the event parameter map using this + * function. Unless it has been logged, in which case it will be destroyed + * automatically when it is logged. + * + * @param[in] event_parameter_map The event parameter map to destroy. + */ +ANALYTICS_API void GoogleAnalytics_EventParameters_Destroy( + GoogleAnalytics_EventParameters* event_parameter_map); + +/** + * @brief Logs an app event. + * + * The event can have up to 25 parameters. Events with the same name must have + * the same parameters. Up to 500 event names are supported. Using predefined + * events and/or parameters is recommended for optimal reporting. + * + * The following event names are reserved and cannot be used: + * - ad_activeview + * - ad_click + * - ad_exposure + * - ad_query + * - ad_reward + * - adunit_exposure + * - app_clear_data + * - app_exception + * - app_remove + * - app_store_refund + * - app_store_subscription_cancel + * - app_store_subscription_convert + * - app_store_subscription_renew + * - app_update + * - app_upgrade + * - dynamic_link_app_open + * - dynamic_link_app_update + * - dynamic_link_first_open + * - error + * - firebase_campaign + * - first_open + * - first_visit + * - in_app_purchase + * - notification_dismiss + * - notification_foreground + * - notification_open + * - notification_receive + * - os_update + * - session_start + * - session_start_with_rollout + * - user_engagement + * + * @param[in] name The name of the event. Should contain 1 to 40 alphanumeric + * characters or underscores. The name must start with an alphabetic + * character. Some event names are reserved. See event_names.h for the list + * of reserved event names. The "firebase_", "google_", and "ga_" prefixes are + * reserved and should not be used. Note that event names are case-sensitive + * and that logging two events whose names differ only in case will result in + * two distinct events. To manually log screen view events, use the + * `screen_view` event name. Must be UTF-8 encoded. + * @param[in] parameters The map of event parameters. Passing `nullptr` + * indicates that the event has no parameters. Parameter names can be up to 40 + * characters long and must start with an alphabetic character and contain + * only alphanumeric characters and underscores. Only String, Int, and Double + * parameter types are supported. String parameter values can be up to 100 + * characters long for standard Google Analytics properties, and up to 500 + * characters long for Google Analytics 360 properties. The "firebase_", + * "google_", and "ga_" prefixes are reserved and should not be used for + * parameter names. The parameter map must be created using the + * GoogleAnalytics_EventParameters_Create() function. Automatically destroyed + * when it is logged. + */ +ANALYTICS_API void GoogleAnalytics_LogEvent( + const char* name, GoogleAnalytics_EventParameters* parameters); + +/** + * @brief Sets a user property to a given value. + * + * Up to 25 user property names are supported. Once set, user property values + * persist throughout the app lifecycle and across sessions. + * + * The following user property names are reserved and cannot be used: + * + * - first_open_time + * - last_deep_link_referrer + * - user_id + * + * @param[in] name The name of the user property to set. Should contain 1 to 24 + * alphanumeric characters or underscores, and must start with an alphabetic + * character. The "firebase_", "google_", and "ga_" prefixes are reserved and + * should not be used for user property names. Must be UTF-8 encoded. + * @param[in] value The value of the user property. Values can be up to 36 + * characters long. Setting the value to `nullptr` remove the user property. + * Must be UTF-8 encoded. + */ +ANALYTICS_API void GoogleAnalytics_SetUserProperty(const char* name, + const char* value); + +/* + * @brief Sets the user ID property. + * + * This feature must be used in accordance with + * Google's Privacy + * Policy + * + * @param[in] user_id The user ID associated with the user of this app on this + * device. The user ID must be non-empty and no more than 256 characters long, + * and UTF-8 encoded. Setting user_id to nullptr removes the user ID. + */ +ANALYTICS_API void GoogleAnalytics_SetUserId(const char* user_id); + +/* + * @brief Clears all analytics data for this instance from the device and resets + * the app instance ID. + */ +ANALYTICS_API void GoogleAnalytics_ResetAnalyticsData(); + +/* + * @brief Sets whether analytics collection is enabled for this app on this + * device. + * + * This setting is persisted across app sessions. By default it is enabled. + * + * @param[in] enabled A flag that enables or disables Analytics collection. + */ +ANALYTICS_API void GoogleAnalytics_SetAnalyticsCollectionEnabled(bool enabled); + +#ifdef __cplusplus +} +#endif + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_C_ANALYTICS_H_ diff --git a/analytics/windows/include/public/event_names.h b/analytics/windows/include/public/event_names.h new file mode 100644 index 0000000000..ef8330751f --- /dev/null +++ b/analytics/windows/include/public/event_names.h @@ -0,0 +1,426 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ + +// Predefined event names. +// +// An Event is an important occurrence in your app that you want to measure. You +// can report up to 500 different types of Events per app and you can associate +// up to 25 unique parameters with each Event type. Some common events are +// suggested below, but you may also choose to specify custom Event types that +// are associated with your specific app. Each event type is identified by a +// unique name. Event names can be up to 40 characters long, may only contain +// alphanumeric characters and underscores ("_"), and must start with an +// alphabetic character. The "firebase_", "google_", and "ga_" prefixes are +// reserved and should not be used. + +namespace firebase::analytics { + +// Ad Impression event. This event signifies when a user sees an ad impression. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterAdPlatform (string) (optional)
  • +//
  • @c kParameterAdFormat (string) (optional)
  • +//
  • @c kParameterAdSource (string) (optional)
  • +//
  • @c kParameterAdUnitName (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventPublicAdImpression[] = "ad_impression"; + +// Add Payment Info event. This event signifies that a user has submitted their +// payment information. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterPaymentType (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddPaymentInfo[] = "add_payment_info"; + +// Add Shipping Info event. This event signifies that a user has submitted their +// shipping information. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterShippingTier (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddShippingInfo[] = "add_shipping_info"; + +// Add to Cart event. This event signifies that an item was added to a cart for +// purchase. Add this event to a funnel with @c kEventPurchase to gauge the +// effectiveness of your checkout process. Note: If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddToCart[] = "add_to_cart"; + +// Add to Wishlist event. This event signifies that an item was added to a +// wishlist. Use this event to identify popular gift items. Note: If you supply +// the @c kParameterValue parameter, you must also supply the @c +// kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventAddToWishlist[] = "add_to_wishlist"; + +// App Open event. By logging this event when an App becomes active, developers +// can understand how often users leave and return during the course of a +// Session. Although Sessions are automatically reported, this event can provide +// further clarification around the continuous engagement of app-users. +inline constexpr char kEventAppOpen[] = "app_open"; + +// E-Commerce Begin Checkout event. This event signifies that a user has begun +// the process of checking out. Add this event to a funnel with your @c +// kEventPurchase event to gauge the effectiveness of your checkout process. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
    +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventBeginCheckout[] = "begin_checkout"; + +// Campaign Detail event. Log this event to supply the referral details of a +// re-engagement campaign. Note: you must supply at least one of the required +// parameters kParameterSource, kParameterMedium or kParameterCampaign. Params: +// +//
    +//
  • @c kParameterSource (string)
  • +//
  • @c kParameterMedium (string)
  • +//
  • @c kParameterCampaign (string)
  • +//
  • @c kParameterTerm (string) (optional)
  • +//
  • @c kParameterContent (string) (optional)
  • +//
  • @c kParameterAdNetworkClickId (string) (optional)
  • +//
  • @c kParameterCP1 (string) (optional)
  • +//
+inline constexpr char kEventCampaignDetails[] = "campaign_details"; + +// Earn Virtual Currency event. This event tracks the awarding of virtual +// currency in your app. Log this along with @c kEventSpendVirtualCurrency to +// better understand your virtual economy. Params: +// +//
    +//
  • @c kParameterVirtualCurrencyName (string)
  • +//
  • @c kParameterValue (signed 64-bit integer or double)
  • +//
+inline constexpr char kEventEarnVirtualCurrency[] = "earn_virtual_currency"; + +// Generate Lead event. Log this event when a lead has been generated in the app +// to understand the efficacy of your install and re-engagement campaigns. Note: +// If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
    +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +//
+inline constexpr char kEventGenerateLead[] = "generate_lead"; + +// Join Group event. Log this event when a user joins a group such as a guild, +// team or family. Use this event to analyze how popular certain groups or +// social features are in your app. Params: +// +//
    +//
  • @c kParameterGroupId (string)
  • +//
+inline constexpr char kEventJoinGroup[] = "join_group"; + +// Level Start event. Log this event when the user starts a level. Params: +// +//
    +//
  • @c kParameterLevelName (string)
  • +//
  • @c kParameterSuccess (string)
  • +//
+inline constexpr char kEventLevelEnd[] = "level_end"; + +// Level Up event. Log this event when the user finishes a level. Params: +// +//
    +//
  • @c kParameterLevelName (string)
  • +//
  • @c kParameterSuccess (string)
  • +//
+inline constexpr char kEventLevelStart[] = "level_start"; + +// Level Up event. This event signifies that a player has leveled up in your +// gaming app. It can help you gauge the level distribution of your userbase +// and help you identify certain levels that are difficult to pass. Params: +// +//
    +//
  • @c kParameterLevel (signed 64-bit integer)
  • +//
  • @c kParameterCharacter (string) (optional)
  • +//
+inline constexpr char kEventLevelUp[] = "level_up"; + +// Login event. Apps with a login feature can report this event to signify that +// a user has logged in. +inline constexpr char kEventLogin[] = "login"; + +// Post Score event. Log this event when the user posts a score in your gaming +// app. This event can help you understand how users are actually performing in +// your game and it can help you correlate high scores with certain audiences or +// behaviors. Params: +// +//
    +//
  • @c kParameterScore (signed 64-bit integer)
  • +//
  • @c kParameterLevel (signed 64-bit integer) (optional)
  • +//
  • @c kParameterCharacter (string) (optional)
  • +//
+//
  • @c kParameterAffiliation (string) (optional)
  • +//
  • @c kParameterCoupon (string) (optional)
  • +//
  • @c kParameterCurrency (string) (optional)
  • +//
  • @c kParameterItems (array) (optional)
  • +//
  • @c kParameterShipping (double) (optional)
  • +//
  • @c kParameterTax (double) (optional)
  • +//
  • @c kParameterTransactionId (string) (optional)
  • +//
  • @c kParameterValue (double) (optional)
  • +// +inline constexpr char kEventPurchase[] = "purchase"; + +// E-Commerce Refund event. This event signifies that a refund was issued. +// Note: If you supply the @c kParameterValue parameter, you must also supply +// the @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
      +//
    • @c kParameterAffiliation (string) (optional)
    • +//
    • @c kParameterCoupon (string) (optional)
    • +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterShipping (double) (optional)
    • +//
    • @c kParameterTax (double) (optional)
    • +//
    • @c kParameterTransactionId (string) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventRefund[] = "refund"; + +// E-Commerce Remove from Cart event. This event signifies that an item(s) was +// removed from a cart. Note: If you supply the @c kParameterValue parameter, +// you must also supply the @c kParameterCurrency parameter so that revenue +// metrics can be computed accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventRemoveFromCart[] = "remove_from_cart"; + +// Screen View event. This event signifies that a screen in your app has +// appeared. Use this event to contextualize Events that occur on a specific +// screen. Note: The @c kParameterScreenName parameter is optional, and the @c +// kParameterScreenClass parameter is required. If the @c kParameterScreenClass +// is not provided, or if there are extra parameters, the call to log this event +// will be ignored. Params: +// +//
      +//
    • @c kParameterScreenClass (string) (required)
    • +//
    • @c kParameterScreenName (string) (optional)
    • +//
    +inline constexpr char kEventScreenView[] = "screen_view"; + +// Search event. Apps that support search features can use this event to +// contextualize search operations by supplying the appropriate, corresponding +// parameters. This event can help you identify the most popular content in your +// app. Params: +// +//
      +//
    • @c kParameterSearchTerm (string)
    • +//
    • @c kParameterStartDate (string) (optional)
    • +//
    • @c kParameterEndDate (string) (optional)
    • +//
    • @c kParameterNumberOfNights (signed 64-bit integer) +// (optional) for hotel bookings
    • +//
    • @c kParameterNumberOfRooms (signed 64-bit integer) +// (optional) for hotel bookings
    • +//
    • @c kParameterNumberOfPassengers (signed 64-bit integer) +// (optional) for travel bookings
    • +//
    • @c kParameterOrigin (string) (optional)
    • +//
    • @c kParameterDestination (string) (optional)
    • +//
    • @c kParameterTravelClass (string) (optional) for travel bookings
    • +//
    +inline constexpr char kEventSearch[] = "search"; + +// Select Content event. This general purpose event signifies that a user has +// selected some content of a certain type in an app. The content can be any +// object in your app. This event can help you identify popular content and +// categories of content in your app. Params: +// +//
      +//
    • @c kParameterContentType (string)
    • +//
    • @c kParameterItemId (string)
    • +//
    +inline constexpr char kEventSelectContent[] = "select_content"; + +// Select Item event. This event signifies that an item was selected by a user +// from a list. Use the appropriate parameters to contextualize the event. Use +// this event to discover the most popular items selected. Params: +// +//
      +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterItemListId (string) (optional)
    • +//
    • @c kParameterItemListName (string) (optional)
    • +//
    +inline constexpr char kEventSelectItem[] = "select_item"; + +// Select Promotion event. This event signifies that a user has selected a +// promotion offer. Use the appropriate parameters to contextualize the event, +// such as the item(s) for which the promotion applies. Params: +// +//
      +//
    • @c kParameterCreativeName (string) (optional)
    • +//
    • @c kParameterCreativeSlot (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterLocationId (string) (optional)
    • +//
    • @c kParameterPromotionId (string) (optional)
    • +//
    • @c kParameterPromotionName (string) (optional)
    • +//
    +inline constexpr char kEventSelectPromotion[] = "select_promotion"; + +// Share event. Apps with social features can log the Share event to identify +// the most viral content. Params: +// +//
      +//
    • @c kParameterContentType (string)
    • +//
    • @c kParameterItemId (string)
    • +//
    +inline constexpr char kEventShare[] = "share"; + +// Sign Up event. This event indicates that a user has signed up for an account +// in your app. The parameter signifies the method by which the user signed up. +// Use this event to understand the different behaviors between logged in and +// logged out users. Params: +// +//
      +//
    • @c kParameterSignUpMethod (string)
    • +//
    +inline constexpr char kEventSignUp[] = "sign_up"; + +// Spend Virtual Currency event. This event tracks the sale of virtual goods in +// your app and can help you identify which virtual goods are the most popular +// objects of purchase. Params: +// +//
      +//
    • @c kParameterItemName (string)
    • +//
    • @c kParameterVirtualCurrencyName (string)
    • +//
    • @c kParameterValue (signed 64-bit integer or double)
    • +//
    +inline constexpr char kEventSpendVirtualCurrency[] = "spend_virtual_currency"; + +// Tutorial Begin event. This event signifies the start of the on-boarding +// process in your app. Use this in a funnel with kEventTutorialComplete to +// understand how many users complete this process and move on to the full app +// experience. +inline constexpr char kEventTutorialBegin[] = "tutorial_begin"; + +// Tutorial End event. Use this event to signify the user's completion of your +// app's on-boarding process. Add this to a funnel with kEventTutorialBegin to +// gauge the completion rate of your on-boarding process. +inline constexpr char kEventTutorialComplete[] = "tutorial_complete"; + +// Unlock Achievement event. Log this event when the user has unlocked an +// achievement in your game. Since achievements generally represent the breadth +// of a gaming experience, this event can help you understand how many users +// are experiencing all that your game has to offer. Params: +// +//
      +//
    • @c kParameterAchievementId (string)
    • +//
    +inline constexpr char kEventUnlockAchievement[] = "unlock_achievement"; + +// E-commerce View Cart event. This event signifies that a user has viewed +// their cart. Use this to analyze your purchase funnel. Note: If you supply +// the @c kParameterValue parameter, you must also supply the +// @c kParameterCurrency parameter so that revenue metrics can be computed +// accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventViewCart[] = "view_cart"; + +// View Item event. This event signifies that a user has viewed an item. Use +// the appropriate parameters to contextualize the event. Use this event to +// discover the most popular items viewed in your app. Note: If you supply the +// @c kParameterValue parameter, you must also supply the @c kParameterCurrency +// parameter so that revenue metrics can be computed accurately. Params: +// +//
      +//
    • @c kParameterCurrency (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterValue (double) (optional)
    • +//
    +inline constexpr char kEventViewItem[] = "view_item"; + +// View Item List event. Log this event when a user sees a list of items or +// offerings. Params: +// +//
      +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterItemListId (string) (optional)
    • +//
    • @c kParameterItemListName (string) (optional)
    • +//
    +inline constexpr char kEventViewItemList[] = "view_item_list"; + +// View Promotion event. This event signifies that a promotion was shown to a +// user. Add this event to a funnel with the @c kEventAddToCart and +// @c kEventPurchase to gauge your conversion process. Params: +// +//
      +//
    • @c kParameterCreativeName (string) (optional)
    • +//
    • @c kParameterCreativeSlot (string) (optional)
    • +//
    • @c kParameterItems (array) (optional)
    • +//
    • @c kParameterLocationId (string) (optional)
    • +//
    • @c kParameterPromotionId (string) (optional)
    • +//
    • @c kParameterPromotionName (string) (optional)
    • +//
    +inline constexpr char kEventViewPromotion[] = "view_promotion"; + +// View Search Results event. Log this event when the user has been presented +// with the results of a search. Params: +// +//
      +//
    • @c kParameterSearchTerm (string)
    • +//
    +inline constexpr char kEventViewSearchResults[] = "view_search_results"; + +} // namespace firebase::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_EVENT_NAMES_H_ diff --git a/analytics/windows/include/public/parameter_names.h b/analytics/windows/include/public/parameter_names.h new file mode 100644 index 0000000000..7c5f45695e --- /dev/null +++ b/analytics/windows/include/public/parameter_names.h @@ -0,0 +1,253 @@ +// Copyright 2025 Google LLC +#ifndef ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ +#define ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ + +namespace firebase::analytics { + +// Game achievement ID (string). +inline constexpr char kParameterAchievementId[] = "achievement_id"; + +// The ad format (e.g. Banner, Interstitial, Rewarded, Native, Rewarded +// Interstitial, Instream). (string). +inline constexpr char kParameterAdFormat[] = "ad_format"; + +// Ad Network Click ID (string). Used for network-specific click IDs which vary +// in format. +inline constexpr char kParameterAdNetworkClickId[] = "aclid"; + +// The ad platform (e.g. MoPub, IronSource) (string). +inline constexpr char kParameterAdPlatform[] = "ad_platform"; + +// The ad source (e.g. AdColony) (string). +inline constexpr char kParameterAdSource[] = "ad_source"; + +// The ad unit name (e.g. Banner_03) (string). +inline constexpr char kParameterAdUnitName[] = "ad_unit_name"; + +// A product affiliation to designate a supplying company or brick and mortar +// store location (string). +inline constexpr char kParameterAffiliation[] = "affiliation"; + +// Campaign custom parameter (string). Used as a method of capturing custom data +// in a campaign. Use varies by network. +inline constexpr char kParameterCP1[] = "cp1"; + +// The individual campaign name, slogan, promo code, etc. Some networks have +// pre-defined macro to capture campaign information, otherwise can be populated +// by developer. Highly Recommended (string). +inline constexpr char kParameterCampaign[] = "campaign"; + +// Campaign ID (string). Used for keyword analysis to identify a specific +// product promotion or strategic campaign. This is a required key for GA4 data +// import. +inline constexpr char kParameterCampaignId[] = "campaign_id"; + +// Character used in game (string). +inline constexpr char kParameterCharacter[] = "character"; + +// Campaign content (string). +inline constexpr char kParameterContent[] = "content"; + +// Type of content selected (string). +inline constexpr char kParameterContentType[] = "content_type"; + +// Coupon code used for a purchase (string). +inline constexpr char kParameterCoupon[] = "coupon"; + +// Creative Format (string). Used to identify the high-level classification of +// the type of ad served by a specific campaign. +inline constexpr char kParameterCreativeFormat[] = "creative_format"; + +// The name of a creative used in a promotional spot (string). +inline constexpr char kParameterCreativeName[] = "creative_name"; + +// The name of a creative slot (string). +inline constexpr char kParameterCreativeSlot[] = "creative_slot"; + +// Currency of the purchase or items associated with the event, in 3-letter +// ISO_4217 format (string). +inline constexpr char kParameterCurrency[] = "currency"; + +// Flight or Travel destination (string). +inline constexpr char kParameterDestination[] = "destination"; + +// Monetary value of discount associated with a purchase (double). +inline constexpr char kParameterDiscount[] = "discount"; + +// The arrival date, check-out date or rental end date for the item. This should +// be in YYYY-MM-DD format (string). +inline constexpr char kParameterEndDate[] = "end_date"; + +// Indicates that the associated event should either extend the current session +// or start a new session if no session was active when the event was logged. +// Specify 1 to extend the current session or to start a new session; any other +// value will not extend or start a session. +inline constexpr char kParameterExtendSession[] = "extend_session"; + +// Flight or Travel origin (string). +inline constexpr char kParameterFlightNumber[] = "flight_number"; + +// Group/clan/guild ID (string). +inline constexpr char kParameterGroupId[] = "group_id"; + +// Index of an item in a list (integer). +inline constexpr char kParameterIndex[] = "index"; + +// Item brand (string). +inline constexpr char kParameterItemBrand[] = "item_brand"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory[] = "item_category"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory2[] = "item_category2"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory3[] = "item_category3"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory4[] = "item_category4"; + +// Item category (context-specific) (string). +inline constexpr char kParameterItemCategory5[] = "item_category5"; + +// Item ID (context-specific) (string). +inline constexpr char kParameterItemId[] = "item_id"; + +// The ID of the list in which the item was presented to the user (string). +inline constexpr char kParameterItemListId[] = "item_list_id"; + +// The name of the list in which the item was presented to the user (string). +inline constexpr char kParameterItemListName[] = "item_list_name"; + +// Item Name (context-specific) (string). +inline constexpr char kParameterItemName[] = "item_name"; + +// Item variant (string). +inline constexpr char kParameterItemVariant[] = "item_variant"; + +// The list of items involved in the transaction expressed as `[[String: Any]]`. +inline constexpr char kParameterItems[] = "items"; + +// Level in game (integer). +inline constexpr char kParameterLevel[] = "level"; + +// Location (string). The Google Place ID +// that corresponds to the associated event. Alternatively, you can supply +// your own custom Location ID. +inline constexpr char kParameterLocation[] = "location"; + +// The location associated with the event. Preferred to be the Google +// Place ID that +// corresponds to the associated item but could be overridden to a custom +// location ID string.(string). +inline constexpr char kParameterLocationId[] = "location_id"; + +// Marketing Tactic (string). Used to identify the targeting criteria applied to +// a specific campaign. +inline constexpr char kParameterMarketingTactic[] = "marketing_tactic"; + +// The method used to perform an operation (string). +inline constexpr char kParameterMethod[] = "method"; + +// Number of nights staying at hotel (integer). +inline constexpr char kParameterNumberOfNights[] = "number_of_nights"; + +// Number of passengers traveling (integer). +inline constexpr char kParameterNumberOfPassengers[] = "number_of_passengers"; + +// Number of rooms for travel events (integer). +inline constexpr char kParameterNumberOfRooms[] = "number_of_rooms"; + +// Flight or Travel origin (string). +inline constexpr char kParameterOrigin[] = "origin"; + +// The chosen method of payment (string). +inline constexpr char kParameterPaymentType[] = "payment_type"; + +// Purchase price (double). +inline constexpr char kParameterPrice[] = "price"; + +// The ID of a product promotion (string). +inline constexpr char kParameterPromotionId[] = "promotion_id"; + +// The name of a product promotion (string). +inline constexpr char kParameterPromotionName[] = "promotion_name"; + +// Purchase quantity (integer). +inline constexpr char kParameterQuantity[] = "quantity"; + +// Score in game (integer). +inline constexpr char kParameterScore[] = "score"; + +// Current screen class, such as the class name of the UI view controller, +// logged with screen_view event and added to every event (string). +inline constexpr char kParameterScreenClass[] = "screen_class"; + +// Current screen name, such as the name of the UI view, logged with screen_view +// event and added to every event (string). +inline constexpr char kParameterScreenName[] = "screen_name"; + +// The search string/keywords used (string). +inline constexpr char kParameterSearchTerm[] = "search_term"; + +// Shipping cost associated with a transaction (double). +inline constexpr char kParameterShipping[] = "shipping"; + +// The shipping tier (e.g. Ground, Air, Next-day) selected for delivery of the +// purchased item (string). +inline constexpr char kParameterShippingTier[] = "shipping_tier"; + +// The origin of your traffic, such as an Ad network (for example, google) or +// partner (urban airship). Identify the advertiser, site, publication, etc. +// that is sending traffic to your property. Highly recommended (string). +inline constexpr char kParameterSource[] = "source"; + +// Source Platform (string). Used to identify the platform responsible for +// directing traffic to a given Analytics property (e.g., a buying platform +// where budgets, targeting criteria, etc. are set, a platform for managing +// organic traffic data, etc.). +inline constexpr char kParameterSourcePlatform[] = "source_platform"; + +// The departure date, check-in date or rental start date for the item. This +// should be in YYYY-MM-DD format (string). +inline constexpr char kParameterStartDate[] = "start_date"; + +// The result of an operation. Specify 1 to indicate success and 0 to indicate +// failure (integer). +inline constexpr char kParameterSuccess[] = "success"; + +// Tax cost associated with a transaction (double). +inline constexpr char kParameterTax[] = "tax"; + +// If you're manually tagging keyword campaigns, you should use utm_term to +// specify the keyword (string). +inline constexpr char kParameterTerm[] = "term"; + +// The unique identifier of a transaction (string). +inline constexpr char kParameterTransactionId[] = "transaction_id"; + +// Travel class (string). +inline constexpr char kParameterTravelClass[] = "travel_class"; + +// A context-specific numeric value which is accumulated automatically for each +// event type. This is a general purpose parameter that is useful for +// accumulating a key metric that pertains to an event. Examples include +// revenue, distance, time and points. Value should be specified as integer or +// double. +// Notes: Values for pre-defined currency-related events (such as @c +// kEventAddToCart) should be supplied using Double and must be accompanied by a +// @c kParameterCurrency parameter. The valid range of accumulated values is +// [-9,223,372,036,854.77, 9,223,372,036,854.77]. Supplying a non-numeric value, +// omitting the corresponding @c kParameterCurrency parameter, or supplying an +// invalid currency code for conversion +// events will cause that conversion to be omitted from reporting. +inline constexpr char kParameterValue[] = "value"; + +// The type of virtual currency being used (string). +inline constexpr char kParameterVirtualCurrencyName[] = "virtual_currency_name"; + +} // namespace firebase::analytics + +#endif // ANALYTICS_MOBILE_CONSOLE_MEASUREMENT_PUBLIC_PARAMETER_NAMES_H_ diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 4aefc3a36c..6b1108ac31 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -541,17 +541,6 @@ if (IOS) ${FIREBASE_SOURCE_DIR}/functions/src/include/firebase/functions/callable_reference.h ${FIREBASE_SOURCE_DIR}/functions/src/include/firebase/functions/callable_result.h ${FIREBASE_SOURCE_DIR}/functions/src/include/firebase/functions/common.h) - set(gma_HDRS - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/ad_view.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/interstitial_ad.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/internal/native_ad.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/internal/query_info.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/rewarded_ad.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/types.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/ump.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/ump/consent_info.h - ${FIREBASE_SOURCE_DIR}/gma/src/include/firebase/gma/ump/types.h) set(installations_HDRS ${FIREBASE_SOURCE_DIR}/installations/src/include/firebase/installations.h) set(messaging_HDRS @@ -566,6 +555,10 @@ if (IOS) ${FIREBASE_SOURCE_DIR}/storage/src/include/firebase/storage/listener.h ${FIREBASE_SOURCE_DIR}/storage/src/include/firebase/storage/metadata.h ${FIREBASE_SOURCE_DIR}/storage/src/include/firebase/storage/storage_reference.h) + set(ump_HDRS + ${FIREBASE_SOURCE_DIR}/ump/src/include/firebase/ump.h + ${FIREBASE_SOURCE_DIR}/ump/src/include/firebase/ump/consent_info.h + ${FIREBASE_SOURCE_DIR}/ump/src/include/firebase/ump/types.h) list(APPEND framework_HDRS src/include/firebase/internal/platform.h @@ -576,11 +569,12 @@ if (IOS) ${dynamic_links_HDRS} ${firestore_HDRS} ${functions_HDRS} - ${gma_HDRS} ${installations_HDRS} ${messaging_HDRS} ${remote_config_HDRS} - ${storage_HDRS}) + ${storage_HDRS} + ${ump_HDRS} + ) # add framework header files to target target_sources(firebase_app PRIVATE ${framework_HDRS}) diff --git a/app/app_resources/build.gradle b/app/app_resources/build.gradle index a33339ba8c..00b8acfdda 100644 --- a/app/app_resources/build.gradle +++ b/app/app_resources/build.gradle @@ -56,12 +56,15 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/app/build.gradle b/app/build.gradle index 38dac86f0a..4d2e2275e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,4 +81,7 @@ project.afterEvaluate { setupDexDependencies(':app:app_resources') setupDexDependencies(':app:google_api_resources') setupDexDependencies(':app:invites_resources') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/app/google_api_resources/build.gradle b/app/google_api_resources/build.gradle index fb69648443..ff53f48741 100644 --- a/app/google_api_resources/build.gradle +++ b/app/google_api_resources/build.gradle @@ -59,14 +59,17 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' - implementation 'com.google.android.gms:play-services-base:18.6.0' + implementation 'com.google.android.gms:play-services-base:18.7.0' implementation project(':app:app_resources') } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/app/integration_test/Podfile b/app/integration_test/Podfile index c88e3a06cb..f2d87f5d8f 100644 --- a/app/integration_test/Podfile +++ b/app/integration_test/Podfile @@ -4,7 +4,7 @@ platform :ios, '13.0' use_frameworks! :linkage => :static target 'integration_test' do - pod 'Firebase/Analytics', '11.10.0' + pod 'Firebase/Analytics', '11.14.0' end post_install do |installer| diff --git a/app/integration_test/build.gradle b/app/integration_test/build.gradle index b764da6189..44663a32a2 100644 --- a/app/integration_test/build.gradle +++ b/app/integration_test/build.gradle @@ -96,3 +96,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/app/invites_resources/build.gradle b/app/invites_resources/build.gradle index 4d8db6b442..31f6d284b2 100644 --- a/app/invites_resources/build.gradle +++ b/app/invites_resources/build.gradle @@ -55,7 +55,7 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-dynamic-links' implementation project(':app:app_resources') @@ -63,6 +63,9 @@ dependencies { afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/app/src/invites/ios/invites_ios_startup.mm b/app/src/invites/ios/invites_ios_startup.mm index 4ae901d57a..a99602ba79 100644 --- a/app/src/invites/ios/invites_ios_startup.mm +++ b/app/src/invites/ios/invites_ios_startup.mm @@ -286,7 +286,7 @@ @implementation UIApplication (FIRFBI) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogDebug. NSLog(@"Loading UIApplication category for Firebase App"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { ::firebase::invites::HookAppDelegateMethods(clazz); }); } diff --git a/app/src/util_ios.h b/app/src/util_ios.h index 454fab09cd..1a69484979 100644 --- a/app/src/util_ios.h +++ b/app/src/util_ios.h @@ -185,10 +185,12 @@ typedef BOOL ( id self, SEL selector_value, UIApplication *application, NSUserActivity *user_activity, void (^restoration_handler)(NSArray *)); -// Call the given block once for every Objective-C class that exists that -// implements the UIApplicationDelegate protocol (except for those in a -// blacklist we keep). -void ForEachAppDelegateClass(void (^block)(Class)); +// Calls the given block for each unique Objective-C class that has been +// previously passed to [UIApplication setDelegate:]. The block is executed +// immediately for all currently known unique delegate classes. +// Additionally, the block is queued to be executed if any new, unique +// Objective-C class is passed to [UIApplication setDelegate:] in the future. +void RunOnAppDelegateClasses(void (^block)(Class)); // Convert a string array into an NSMutableArray. NSMutableArray *StringVectorToNSMutableArray( diff --git a/app/src/util_ios.mm b/app/src/util_ios.mm index 21a70e6c85..6286eeae4b 100644 --- a/app/src/util_ios.mm +++ b/app/src/util_ios.mm @@ -27,6 +27,103 @@ #import #import +using firebase::GetLogLevel; +using firebase::kLogLevelDebug; + +// Key used in Info.plist to specify a custom AppDelegate class name. +static NSString *const kFirebaseAppDelegateClassNameKey = @"FirebaseAppDelegateClassName"; + +#define MAX_PENDING_APP_DELEGATE_BLOCKS 8 +#define MAX_SEEN_DELEGATE_CLASSES 32 + +static IMP g_original_setDelegate_imp = NULL; +static void (^g_pending_app_delegate_blocks[MAX_PENDING_APP_DELEGATE_BLOCKS])(Class) = {nil}; +static int g_pending_block_count = 0; +static Class g_seen_delegate_classes[MAX_SEEN_DELEGATE_CLASSES] = {nil}; +static int g_seen_delegate_classes_count = 0; + +static void Firebase_setDelegate(id self, SEL _cmd, id delegate) { + Class new_class = nil; + if (delegate) { + new_class = [delegate class]; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with class %s (Swizzled)", + class_getName(new_class)); + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: UIApplication setDelegate: called with nil delegate (Swizzled)"); + } + + if (new_class) { + // 1. Superclass Check + bool superclass_already_seen = false; + Class current_super = class_getSuperclass(new_class); + while (current_super) { + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == current_super) { + superclass_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s has superclass %s which was already seen. Skipping " + @"processing for %s.", + class_getName(new_class), class_getName(current_super), class_getName(new_class)); + break; + } + } + if (superclass_already_seen) break; + current_super = class_getSuperclass(current_super); + } + + if (!superclass_already_seen) { + // 2. Direct Class Check (if no superclass was seen) + bool direct_class_already_seen = false; + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i] == new_class) { + direct_class_already_seen = true; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Delegate class %s already seen directly. Skipping processing.", + class_getName(new_class)); + break; + } + } + + if (!direct_class_already_seen) { + // 3. Process as New Class + if (g_seen_delegate_classes_count < MAX_SEEN_DELEGATE_CLASSES) { + g_seen_delegate_classes[g_seen_delegate_classes_count] = new_class; + g_seen_delegate_classes_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Added new delegate class %s to seen list (total seen: %d).", + class_getName(new_class), g_seen_delegate_classes_count); + + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Executing %d pending block(s) for new delegate class: %s.", + g_pending_block_count, class_getName(new_class)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](new_class); + // Pending blocks persist to run for future new delegate classes. + } + } + } + } else { + NSLog(@"Firebase Error: Exceeded MAX_SEEN_DELEGATE_CLASSES (%d). Cannot add new delegate " + @"class %s or run pending blocks for it.", + MAX_SEEN_DELEGATE_CLASSES, class_getName(new_class)); + } + } + } + } + + // Call the original setDelegate: implementation + if (g_original_setDelegate_imp) { + ((void (*)(id, SEL, id))g_original_setDelegate_imp)(self, _cmd, + delegate); + } else { + NSLog(@"Firebase Error: Original setDelegate: IMP not found, cannot call original method."); + } +} + @implementation FIRSAMAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -76,41 +173,133 @@ - (BOOL)application:(UIApplication *)application #endif // defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 @end +@implementation UIApplication (FirebaseAppDelegateSwizzling) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *appDelegateClassName = + [[NSBundle mainBundle] objectForInfoDictionaryKey:kFirebaseAppDelegateClassNameKey]; + + if (appDelegateClassName && [appDelegateClassName isKindOfClass:[NSString class]] && + appDelegateClassName.length > 0) { + Class specificClass = NSClassFromString(appDelegateClassName); + if (specificClass) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Info.plist key '%@' found. Targeting AppDelegate class: %@. Swizzling " + @"of [UIApplication setDelegate:] will be skipped.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + + // Set this class as the sole "seen" delegate for Firebase processing. + // g_seen_delegate_classes_count should be 0 here in +load, but clear just in case. + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + g_seen_delegate_classes[i] = nil; + } + g_seen_delegate_classes[0] = specificClass; + g_seen_delegate_classes_count = 1; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: %@ is now the only delegate class Firebase will initially process.", + appDelegateClassName); + + // If there are already blocks pending (e.g., from other Firebase components' +load + // methods), execute them now for the specified delegate. These blocks will remain in the + // pending queue, mirroring the behavior of the original swizzled setDelegate: method which + // also does not clear pending blocks after execution (as they might apply to future + // delegates). In this Info.plist mode, however, Firebase won't process further setDelegate: + // calls. + if (g_pending_block_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Executing %d PENDING block(s) for " + @"specified delegate: %@. (Blocks are not removed from queue).", + g_pending_block_count, NSStringFromClass(specificClass)); + for (int i = 0; i < g_pending_block_count; i++) { + if (g_pending_app_delegate_blocks[i]) { + g_pending_app_delegate_blocks[i](specificClass); + } + } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: +load (Info.plist Mode) - Pending blocks executed for specific " + @"delegate."); + } + // Skip swizzling. g_original_setDelegate_imp remains NULL. + return; + } else { + NSLog(@"Firebase Error: Info.plist key '%@' specifies class '%@', which was not found. " + @"Proceeding with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } + } else { + if (appDelegateClassName) { // Key is present but value is invalid (e.g., empty string or + // wrong type). + NSLog(@"Firebase Error: Info.plist key '%@' has an invalid value ('%@'). Proceeding " + @"with default swizzling.", + kFirebaseAppDelegateClassNameKey, appDelegateClassName); + } else { // Key is not present. + // This is the default case, no special logging needed here beyond the swizzling log itself. + } + } + + // Standard behavior: Swizzle [UIApplication setDelegate:] + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Proceeding with swizzling of [UIApplication setDelegate:]."); + Class uiApplicationClass = [UIApplication class]; + SEL originalSelector = @selector(setDelegate:); + Method originalMethod = class_getInstanceMethod(uiApplicationClass, originalSelector); + + if (!originalMethod) { + NSLog( + @"Firebase Error: Original [UIApplication setDelegate:] method not found for swizzling."); + return; + } + + IMP previousImp = method_setImplementation(originalMethod, (IMP)Firebase_setDelegate); + if (previousImp) { + g_original_setDelegate_imp = previousImp; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Successfully swizzled [UIApplication setDelegate:] and stored original " + @"IMP."); + } else { + NSLog(@"Firebase Error: Swizzled [UIApplication setDelegate:], but original IMP was NULL (or " + @"method_setImplementation failed to return the previous IMP)."); + } + }); +} + +@end + namespace firebase { namespace util { -void ForEachAppDelegateClass(void (^block)(Class)) { - unsigned int number_of_classes; - Class *classes = objc_copyClassList(&number_of_classes); - for (unsigned int i = 0; i < number_of_classes; i++) { - Class clazz = classes[i]; - if (class_conformsToProtocol(clazz, @protocol(UIApplicationDelegate))) { - const char *class_name = class_getName(clazz); - bool blacklisted = false; - static const char *kClassNameBlacklist[] = { - // Declared in Firebase Analytics: - // //googlemac/iPhone/Firebase/Analytics/Sources/ApplicationDelegate/ - // FIRAAppDelegateProxy.m - "FIRAAppDelegate", - // Declared here. - "FIRSAMAppDelegate"}; - for (size_t i = 0; i < FIREBASE_ARRAYSIZE(kClassNameBlacklist); ++i) { - if (strcmp(class_name, kClassNameBlacklist[i]) == 0) { - blacklisted = true; - break; - } - } - if (!blacklisted) { - if (GetLogLevel() <= kLogLevelDebug) { - // Call NSLog directly because we may be in a +load method, - // and C++ classes may not be constructed yet. - NSLog(@"Firebase: Found UIApplicationDelegate class %s", class_name); - } - block(clazz); +void RunOnAppDelegateClasses(void (^block)(Class)) { + if (g_seen_delegate_classes_count > 0) { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses executing block for %d already seen delegate " + @"class(es).", + g_seen_delegate_classes_count); + for (int i = 0; i < g_seen_delegate_classes_count; i++) { + if (g_seen_delegate_classes[i]) { // Safety check + block(g_seen_delegate_classes[i]); } } + } else { + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - no delegate classes seen yet. Block will be " + @"queued for future delegates."); + } + + // Always try to queue the block for any future new delegate classes. + if (g_pending_block_count < MAX_PENDING_APP_DELEGATE_BLOCKS) { + g_pending_app_delegate_blocks[g_pending_block_count] = [block copy]; + g_pending_block_count++; + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: RunOnAppDelegateClasses - added block to pending list (total pending: %d). " + @"This block will run on future new delegate classes.", + g_pending_block_count); + } else { + NSLog(@"Firebase Error: RunOnAppDelegateClasses - pending block queue is full (max %d). Cannot " + @"add new block for future execution. Discarding block.", + MAX_PENDING_APP_DELEGATE_BLOCKS); } - free(classes); } NSDictionary *StringMapToNSDictionary(const std::map &string_map) { @@ -354,8 +543,27 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func const char *class_name = class_getName(clazz); Method method = class_getInstanceMethod(clazz, name); NSString *selector_name_nsstring = NSStringFromSelector(name); - const char *selector_name = selector_name_nsstring.UTF8String; - IMP original_method_implementation = method ? method_getImplementation(method) : nil; + const char *selector_name = selector_name_nsstring.UTF8String; // Used for logging later + + IMP current_actual_imp = method ? method_getImplementation(method) : nil; + + // === Begin idempotency check === + if (current_actual_imp == imp) { + // Assuming GetLogLevel() and kLogLevelDebug are available here. + // Based on previous file content, GetLogLevel is available in this file from util_ios.h. + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method %s on class %s is already swizzled with the target IMP. Skipping " + @"re-swizzle.", + selector_name, class_name); + + return; // Already swizzled to the desired implementation + } + // === End idempotency check === + + // If we reach here, current_actual_imp is different from imp, or the method didn't exist. + // We now assign original_method_implementation to be current_actual_imp for the rest of the + // function. + IMP original_method_implementation = current_actual_imp; // Get the type encoding of the selector from a type_encoding_class (which is a class which // implements a stub for the method). @@ -364,9 +572,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func assert(type_encoding); NSString *new_method_name_nsstring = nil; - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registering method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Attempting to register method for %s selector %s", class_name, selector_name); + if (original_method_implementation) { // Try adding a method with randomized prefix on the name. int retry = kRandomNameGenerationRetries; @@ -381,32 +589,32 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func } const char *new_method_name = new_method_name_nsstring.UTF8String; if (retry == 0) { - NSLog(@"Failed to add method %s on class %s as the %s method already exists on the class. To " - @"resolve this issue, change the name of the method %s on the class %s.", - new_method_name, class_name, new_method_name, new_method_name, class_name); + NSLog( + @"Firebase Error: Failed to add method %s on class %s as the %s method already exists on " + @"the class. To resolve this issue, change the name of the method %s on the class %s.", + new_method_name, class_name, new_method_name, new_method_name, class_name); return; } method_setImplementation(method, imp); // Save the selector name that points at the original method implementation. SetMethod(name, new_method_name_nsstring); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Registered method for %s selector %s (original method %s 0x%08x)", class_name, - selector_name, new_method_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Registered method for %s selector %s (original method %s 0x%08x)", + class_name, selector_name, new_method_name, static_cast(reinterpret_cast(original_method_implementation))); - } + } else if (add_method) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Adding method for %s selector %s", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Adding method for %s selector %s", class_name, selector_name); + // The class doesn't implement the selector so simply install our method implementation. if (!class_addMethod(clazz, name, imp, type_encoding)) { - NSLog(@"Failed to add new method %s on class %s.", selector_name, class_name); + NSLog(@"Firebase Error: Failed to add new method %s on class %s.", selector_name, class_name); } } else { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method implementation for %s selector %s not found, ignoring.", class_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method implementation for %s selector %s not found, ignoring.", class_name, selector_name); - } } } @@ -420,9 +628,9 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func selector_implementation_names_per_selector_[selector_name_nsstring]; const char *class_name = class_getName(clazz); if (!selector_implementation_names) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Method not cached for class %s selector %s.", class_name, selector_name); - } + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Method not cached for class %s selector %s.", class_name, selector_name); + return nil; } @@ -440,10 +648,10 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func search_class = clazz; for (; search_class; search_class = class_getSuperclass(search_class)) { const char *search_class_name = class_getName(search_class); - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Searching for selector %s (%s) on class %s", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Searching for selector %s (%s) on class %s", selector_name, selector_implementation_name, search_class_name); - } + Method method = class_getInstanceMethod(search_class, selector_implementation); method_implementation = method ? method_getImplementation(method) : nil; if (method_implementation) break; @@ -451,18 +659,18 @@ void RunOnBackgroundThread(void (*function_ptr)(void *function_data), void *func if (method_implementation) break; } if (!method_implementation) { - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Class %s does not respond to selector %s (%s)", class_name, selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Class %s does not respond to selector %s (%s)", class_name, selector_name, selector_implementation_name_nsstring.UTF8String); - } + return nil; } - if (GetLogLevel() <= kLogLevelDebug) { - NSLog(@"Found %s (%s, 0x%08x) on class %s (%s)", selector_name, + if (GetLogLevel() <= kLogLevelDebug) + NSLog(@"Firebase: Found %s (%s, 0x%08x) on class %s (%s)", selector_name, selector_implementation_name_nsstring.UTF8String, static_cast(reinterpret_cast(method_implementation)), class_name, class_getName(search_class)); - } + return method_implementation; } diff --git a/app/test_resources/build.gradle b/app/test_resources/build.gradle index 04aebe642b..1850b9fd40 100644 --- a/app/test_resources/build.gradle +++ b/app/test_resources/build.gradle @@ -54,6 +54,9 @@ android { afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/app_check/app_check_resources/build.gradle b/app_check/app_check_resources/build.gradle index 94849cdae1..9df03a0b61 100644 --- a/app_check/app_check_resources/build.gradle +++ b/app_check/app_check_resources/build.gradle @@ -55,12 +55,15 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-appcheck' } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/app_check/build.gradle b/app_check/build.gradle index eb0148237d..fe46f1d9cd 100644 --- a/app_check/build.gradle +++ b/app_check/build.gradle @@ -87,4 +87,7 @@ project.afterEvaluate { generateProguardFile('app_check') setupDexDependencies(':app_check:app_check_resources') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/app_check/integration_test/Podfile b/app_check/integration_test/Podfile index 524eaabf1b..ef70c718ad 100644 --- a/app_check/integration_test/Podfile +++ b/app_check/integration_test/Podfile @@ -4,20 +4,20 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/AppCheck', '11.10.0' - pod 'Firebase/Database', '11.10.0' - pod 'Firebase/Auth', '11.10.0' - pod 'Firebase/Storage', '11.10.0' - pod 'Firebase/Functions', '11.10.0' + pod 'Firebase/AppCheck', '11.14.0' + pod 'Firebase/Database', '11.14.0' + pod 'Firebase/Auth', '11.14.0' + pod 'Firebase/Storage', '11.14.0' + pod 'Firebase/Functions', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/AppCheck', '11.10.0' - pod 'Firebase/Database', '11.10.0' - pod 'Firebase/Auth', '11.10.0' - pod 'Firebase/Storage', '11.10.0' - pod 'Firebase/Functions', '11.10.0' + pod 'Firebase/AppCheck', '11.14.0' + pod 'Firebase/Database', '11.14.0' + pod 'Firebase/Auth', '11.14.0' + pod 'Firebase/Storage', '11.14.0' + pod 'Firebase/Functions', '11.14.0' end post_install do |installer| diff --git a/app_check/integration_test/build.gradle b/app_check/integration_test/build.gradle index 9a0e96b2fd..c14dab02fe 100644 --- a/app_check/integration_test/build.gradle +++ b/app_check/integration_test/build.gradle @@ -113,3 +113,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/auth/auth_resources/build.gradle b/auth/auth_resources/build.gradle index b11f02e86a..0cedb697b4 100644 --- a/auth/auth_resources/build.gradle +++ b/auth/auth_resources/build.gradle @@ -55,7 +55,7 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-auth' implementation project(':app:app_resources') @@ -63,6 +63,9 @@ dependencies { afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/auth/build.gradle b/auth/build.gradle index 2e4764a7d1..fbaca5f1d6 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -87,4 +87,7 @@ project.afterEvaluate { generateProguardFile('auth') setupDexDependencies(':auth:auth_resources') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/auth/integration_test/Podfile b/auth/integration_test/Podfile index 97716ea640..c9891d045a 100644 --- a/auth/integration_test/Podfile +++ b/auth/integration_test/Podfile @@ -4,12 +4,12 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/auth/integration_test/build.gradle b/auth/integration_test/build.gradle index e65135b02a..88ceb6ca9d 100644 --- a/auth/integration_test/build.gradle +++ b/auth/integration_test/build.gradle @@ -100,3 +100,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/build_scripts/ios/build.sh b/build_scripts/ios/build.sh index 12ac66e93b..35f649274a 100755 --- a/build_scripts/ios/build.sh +++ b/build_scripts/ios/build.sh @@ -18,7 +18,7 @@ usage(){ -g, generate Makefiles default: true -c, CMake build default: true example: - build_scripts/ios/build.sh -b ios_build -s . -a arm64,x86_64 -t firebase_gma,firebase_auth -c false" + build_scripts/ios/build.sh -b ios_build -s . -a arm64,x86_64 -t firebase_auth -c false" } set -e @@ -27,7 +27,7 @@ readonly SUPPORTED_PLATFORMS=(device simulator) readonly SUPPORTED_ARCHITECTURES=(arm64 x86_64) readonly DEVICE_ARCHITECTURES=(arm64) readonly SIMULATOR_ARCHITECTURES=(arm64 x86_64) -readonly SUPPORTED_TARGETS=(firebase_analytics firebase_auth firebase_app_check firebase_database firebase_dynamic_links firebase_firestore firebase_functions firebase_gma firebase_installations firebase_messaging firebase_remote_config firebase_storage) +readonly SUPPORTED_TARGETS=(firebase_analytics firebase_auth firebase_app_check firebase_database firebase_dynamic_links firebase_firestore firebase_functions firebase_installations firebase_messaging firebase_remote_config firebase_storage firebase_ump) # build default value buildpath="ios_build" diff --git a/build_scripts/packaging.conf b/build_scripts/packaging.conf index fd082f4b54..5010b2c4a2 100644 --- a/build_scripts/packaging.conf +++ b/build_scripts/packaging.conf @@ -2,5 +2,5 @@ # List of all Firebase products to include in the binary SDK package. readonly -a product_list=(analytics app app_check auth database -dynamic_links firestore functions gma installations messaging -remote_config storage) +dynamic_links firestore functions installations messaging +remote_config storage ump) diff --git a/cmake/external/firestore.cmake b/cmake/external/firestore.cmake index 89f700b0b3..a9083fc23d 100644 --- a/cmake/external/firestore.cmake +++ b/cmake/external/firestore.cmake @@ -21,7 +21,7 @@ endif() # If the format of the line below changes, then be sure to update # https://github.com/firebase/firebase-cpp-sdk/blob/fd054fa016/.github/workflows/update-dependencies.yml#L81 #set(version CocoaPods-11.8.1) -set(version CocoaPods-11.10.0) +set(version CocoaPods-11.14.0) function(GetReleasedDep) message("Getting released firebase-ios-sdk @ ${version}") diff --git a/cmake/external/leveldb.cmake b/cmake/external/leveldb.cmake index 3dd38315a8..bb6763a299 100644 --- a/cmake/external/leveldb.cmake +++ b/cmake/external/leveldb.cmake @@ -18,8 +18,10 @@ if(TARGET leveldb) return() endif() +if (DESKTOP AND MSVC) set(patch_file ${CMAKE_CURRENT_LIST_DIR}/../../scripts/git/patches/leveldb/0001-leveldb-1.23-windows-paths.patch) +endif() # This version must be kept in sync with the version in firestore.patch.txt. # If this version ever changes then make sure to update the version in diff --git a/cpp_sdk_version.json b/cpp_sdk_version.json index 8384bae520..b8a27dc183 100644 --- a/cpp_sdk_version.json +++ b/cpp_sdk_version.json @@ -1,5 +1,5 @@ { - "released": "12.7.0", - "stable": "12.7.0", - "head": "12.7.0" + "released": "12.8.0", + "stable": "12.8.0", + "head": "12.8.0" } diff --git a/database/build.gradle b/database/build.gradle index 79cc5eca33..299e89cc60 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -88,4 +88,7 @@ project.afterEvaluate { setupDexDependencies(':database:database_resources') preBuild.dependsOn(':app:build') preBuild.dependsOn(':auth:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/database/database_resources/build.gradle b/database/database_resources/build.gradle index cfa2d3408f..74d8efc7e7 100644 --- a/database/database_resources/build.gradle +++ b/database/database_resources/build.gradle @@ -51,7 +51,7 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-database' //implementation project(':app:app_resources') @@ -59,6 +59,9 @@ dependencies { afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/database/integration_test/Podfile b/database/integration_test/Podfile index 6b234d2b48..242df18d83 100644 --- a/database/integration_test/Podfile +++ b/database/integration_test/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Database', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Database', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Database', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Database', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/database/integration_test/build.gradle b/database/integration_test/build.gradle index 9f3937a0a9..ba2d8b3675 100644 --- a/database/integration_test/build.gradle +++ b/database/integration_test/build.gradle @@ -101,3 +101,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/dynamic_links/build.gradle b/dynamic_links/build.gradle index ecf9231331..6fd8088c25 100644 --- a/dynamic_links/build.gradle +++ b/dynamic_links/build.gradle @@ -85,4 +85,7 @@ apply from: "$rootDir/android_build_files/generate_proguard.gradle" project.afterEvaluate { generateProguardFile('dynamic_links') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/dynamic_links/integration_test/Podfile b/dynamic_links/integration_test/Podfile index 78925b5c21..d6a404cee9 100644 --- a/dynamic_links/integration_test/Podfile +++ b/dynamic_links/integration_test/Podfile @@ -5,7 +5,7 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/DynamicLinks', '11.10.0' + pod 'Firebase/DynamicLinks', '11.14.0' end post_install do |installer| diff --git a/dynamic_links/integration_test/build.gradle b/dynamic_links/integration_test/build.gradle index 5c5be7e10c..65c65fc06b 100644 --- a/dynamic_links/integration_test/build.gradle +++ b/dynamic_links/integration_test/build.gradle @@ -96,3 +96,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/firestore/build.gradle b/firestore/build.gradle index c09891e92e..de5fb63c57 100644 --- a/firestore/build.gradle +++ b/firestore/build.gradle @@ -89,4 +89,7 @@ project.afterEvaluate { setupDexDependencies(':firestore:firestore_resources') preBuild.dependsOn(':app:build') preBuild.dependsOn(':auth:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/firestore/firestore_resources/build.gradle b/firestore/firestore_resources/build.gradle index f03cb4298f..339121b1b8 100644 --- a/firestore/firestore_resources/build.gradle +++ b/firestore/firestore_resources/build.gradle @@ -59,13 +59,16 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-firestore' } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/firestore/integration_test/Podfile b/firestore/integration_test/Podfile index e7c9a8d8b9..5b6087db7b 100644 --- a/firestore/integration_test/Podfile +++ b/firestore/integration_test/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Firestore', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Firestore', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Firestore', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Firestore', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/firestore/integration_test/build.gradle b/firestore/integration_test/build.gradle index 2a08e24b6e..19c75410cd 100644 --- a/firestore/integration_test/build.gradle +++ b/firestore/integration_test/build.gradle @@ -101,3 +101,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/firestore/integration_test_internal/Podfile b/firestore/integration_test_internal/Podfile index 3a152f4488..c53730e4f7 100644 --- a/firestore/integration_test_internal/Podfile +++ b/firestore/integration_test_internal/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Firestore', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Firestore', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Firestore', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Firestore', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/firestore/integration_test_internal/build.gradle b/firestore/integration_test_internal/build.gradle index d7c578e467..5570e90f45 100644 --- a/firestore/integration_test_internal/build.gradle +++ b/firestore/integration_test_internal/build.gradle @@ -119,3 +119,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/functions/build.gradle b/functions/build.gradle index 67722579a7..8be960eb3b 100644 --- a/functions/build.gradle +++ b/functions/build.gradle @@ -86,4 +86,7 @@ project.afterEvaluate { generateProguardFile('functions') preBuild.dependsOn(':app:build') preBuild.dependsOn(':auth:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/functions/integration_test/Podfile b/functions/integration_test/Podfile index 0328f84c9e..6b0132447c 100644 --- a/functions/integration_test/Podfile +++ b/functions/integration_test/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Functions', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Functions', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Functions', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Functions', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/functions/integration_test/build.gradle b/functions/integration_test/build.gradle index 48f346165f..d88b4413da 100644 --- a/functions/integration_test/build.gradle +++ b/functions/integration_test/build.gradle @@ -101,3 +101,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/gma/build.gradle b/gma/build.gradle index 45421c7fe0..9b54eea9f6 100644 --- a/gma/build.gradle +++ b/gma/build.gradle @@ -87,4 +87,7 @@ project.afterEvaluate { generateProguardFile('gma') setupDexDependencies(':gma:gma_resources') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/gma/gma_resources/build.gradle b/gma/gma_resources/build.gradle index 62140c669a..dc9007d063 100644 --- a/gma/gma_resources/build.gradle +++ b/gma/gma_resources/build.gradle @@ -56,7 +56,7 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.android.gms:play-services-ads:23.0.0' implementation 'com.google.android.ump:user-messaging-platform:2.2.0' @@ -64,6 +64,9 @@ dependencies { afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/gma/integration_test/Podfile b/gma/integration_test/Podfile index 932579bd3b..c0d08178b1 100644 --- a/gma/integration_test/Podfile +++ b/gma/integration_test/Podfile @@ -5,7 +5,7 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/CoreOnly', '11.10.0' + pod 'Firebase/CoreOnly', '11.14.0' pod 'Google-Mobile-Ads-SDK', '11.2.0' pod 'GoogleUserMessagingPlatform', '2.3.0' end diff --git a/gma/integration_test/build.gradle b/gma/integration_test/build.gradle index 41d156e8eb..16f9d2b87e 100644 --- a/gma/integration_test/build.gradle +++ b/gma/integration_test/build.gradle @@ -100,3 +100,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/gma/src/include/firebase/gma/ump/consent_info.h b/gma/src/include/firebase/gma/ump/consent_info.h index 8f69918ed0..40b80baef3 100644 --- a/gma/src/include/firebase/gma/ump/consent_info.h +++ b/gma/src/include/firebase/gma/ump/consent_info.h @@ -20,6 +20,7 @@ #include "firebase/app.h" #include "firebase/future.h" #include "firebase/gma/ump/types.h" +#include "firebase/internal/common.h" #include "firebase/internal/platform.h" #if FIREBASE_PLATFORM_ANDROID @@ -32,6 +33,9 @@ namespace gma { /// /// The User Messaging Platform (UMP) SDK is Google’s option to handle user /// privacy and consent in mobile apps. +/// +/// @deprecated The firebase::gma::ump namespace has been deprecated and +/// renamed to firebase::ump. namespace ump { namespace internal { @@ -46,6 +50,8 @@ class ConsentInfoInternal; /// /// This class contains all of the methods necessary for obtaining /// consent from the user. +/// +/// @deprecated This class has been moved to the firebase::ump namespace. class ConsentInfo { public: /// Shut down the User Messaging Platform Consent SDK. @@ -64,6 +70,9 @@ class ConsentInfo { /// initialized, nullptr otherwise. Each call to GetInstance() will return the /// same pointer; when you are finished using the SDK, you can delete the /// pointer and the UMP SDK will shut down. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED static ConsentInfo* GetInstance(const ::firebase::App& app, InitResult* init_result_out = nullptr); @@ -93,6 +102,7 @@ class ConsentInfo { /// initialized, nullptr otherwise. Each call to GetInstance() will return the /// same pointer; when you are finished using the SDK, you can delete the /// pointer and the UMP SDK will shut down. + FIREBASE_DEPRECATED static ConsentInfo* GetInstance(JNIEnv* jni_env, jobject activity, InitResult* init_result_out = nullptr); @@ -101,6 +111,7 @@ class ConsentInfo { // existing ConsentInfo instance after it's first initialized. Returns nullptr // if no instance has been created yet; make sure you have called // GetInstance(JNIEnv*, jobject) first. + FIREBASE_DEPRECATED static ConsentInfo* GetInstance(); #endif // defined(DOXYGEN) #endif // FIREBASE_PLATFORM_ANDROID || defined(DOXYGEN) @@ -120,12 +131,18 @@ class ConsentInfo { /// /// @note Once any overload of ConsentInfo::GetInstance has been called, you /// can use this method to obtain the same instance again. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED static ConsentInfo* GetInstance(InitResult* init_result_out = nullptr); #endif // !defined(__ANDROID__) || defined(DOXYGEN) /// The user’s consent status. This value defaults to kConsentStatusUnknown /// until RequestConsentInfoUpdate() is called, and defaults to the previous /// session’s value until RequestConsentInfoUpdate() completes. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED ConsentStatus GetConsentStatus(); /// Requests consent information update. Must be called in every app session @@ -134,20 +151,35 @@ class ConsentInfo { /// updated immediately to hold the consent state from the previous app /// session, if one exists. GetConsentStatus() and CanRequestAds() may be /// updated again immediately before the returned future is completed. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future RequestConsentInfoUpdate(const ConsentRequestParameters& params); /// Get the Future from the most recent call to RequestConsentInfoUpdate(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future RequestConsentInfoUpdateLastResult(); /// Consent form status. This value defaults to kConsentFormStatusUnknown and /// requires a call to RequestConsentInfoUpdate() to update. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED ConsentFormStatus GetConsentFormStatus(); /// Loads a consent form. Returns an error if the consent form is unavailable /// or cannot be loaded. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future LoadConsentForm(); /// Get the Future from the most recent call to LoadConsentForm(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future LoadConsentFormLastResult(); /// Presents the full screen consent form using the given FormParent, which is @@ -163,9 +195,15 @@ class ConsentInfo { /// /// @note You must call LoadConsentForm() and wait for it to complete before /// calling this method. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future ShowConsentForm(FormParent parent); /// Get the Future from the most recent call to ShowConsentForm(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future ShowConsentFormLastResult(); /// Loads a consent form and immediately presents it using the given @@ -180,14 +218,23 @@ class ConsentInfo { /// /// @param[in] parent A FormParent, which is an Activity object on Android and /// a UIViewController object on iOS. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future LoadAndShowConsentFormIfRequired(FormParent parent); /// Get the Future from the most recent call to /// LoadAndShowConsentFormIfRequired(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future LoadAndShowConsentFormIfRequiredLastResult(); /// Check whether the privacy options form needs to be displayed. /// This is updated by RequestConsentInfoUpdate(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus(); /// If GetPrivacyOptionsRequirementStatus() is @@ -206,19 +253,31 @@ class ConsentInfo { /// /// @param[in] parent A FormParent, which is an Activity object on Android and /// a UIViewController object on iOS. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future ShowPrivacyOptionsForm(FormParent parent); /// Get the Future from the most recent call to ShowPrivacyOptionsForm(). + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED Future ShowPrivacyOptionsFormLastResult(); /// Indicates whether the app has completed the necessary steps for gathering /// updated user consent. Returns true if RequestConsentInfoUpdate() has been /// called and GetConsentStatus returns either kConsentStatusNotRequired or /// kConsentStatusObtained. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED bool CanRequestAds(); /// Clears all consent state from persistent storage. This can be used in /// development to simulate a new installation. + /// + /// @deprecated This class has been moved to the firebase::ump namespace. + FIREBASE_DEPRECATED void Reset(); private: diff --git a/gma/src/include/firebase/gma/ump/types.h b/gma/src/include/firebase/gma/ump/types.h index 0684858582..cce119f56b 100644 --- a/gma/src/include/firebase/gma/ump/types.h +++ b/gma/src/include/firebase/gma/ump/types.h @@ -21,6 +21,7 @@ #include #include +#include "firebase/internal/common.h" #include "firebase/internal/platform.h" #if FIREBASE_PLATFORM_ANDROID @@ -37,6 +38,8 @@ namespace gma { namespace ump { /// Debug values for testing geography. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum ConsentDebugGeography { /// Disable geography debugging. kConsentDebugGeographyDisabled = 0, @@ -49,6 +52,8 @@ enum ConsentDebugGeography { /// Debug settings for `ConsentInfo::RequestConsentInfoUpdate()`. These let you /// force a specific geographic location. Be sure to include debug device IDs to /// enable this on hardware. Debug features are always enabled for simulators. +/// +/// @deprecated This struct has been moved to the firebase::ump namespace. struct ConsentDebugSettings { /// Create a default debug setting, with debugging disabled. ConsentDebugSettings() : debug_geography(kConsentDebugGeographyDisabled) {} @@ -61,6 +66,8 @@ struct ConsentDebugSettings { }; /// Parameters for the `ConsentInfo::RequestConsentInfoUpdate()` operation. +/// +/// @deprecated This struct has been moved to the firebase::ump namespace. struct ConsentRequestParameters { ConsentRequestParameters() : tag_for_under_age_of_consent(false) {} @@ -92,6 +99,8 @@ typedef void* FormParent; // FIREBASE_PLATFORM_TVOS /// Consent status values. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum ConsentStatus { /// Unknown status, e.g. prior to calling Request, or if the request fails. kConsentStatusUnknown = 0, @@ -104,6 +113,8 @@ enum ConsentStatus { }; /// Errors that can occur during a RequestConsentInfoUpdate operation. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum ConsentRequestError { /// The operation succeeded. kConsentRequestSuccess = 0, @@ -126,6 +137,8 @@ enum ConsentRequestError { }; /// Status of the consent form, whether it is available to show or not. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum ConsentFormStatus { /// Status is unknown. Call `ConsentInfo::RequestConsentInfoUpdate()` to /// update this. @@ -139,6 +152,8 @@ enum ConsentFormStatus { }; /// Errors when loading or showing the consent form. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum ConsentFormError { /// The operation succeeded. kConsentFormSuccess = 0, @@ -161,6 +176,8 @@ enum ConsentFormError { }; /// Whether the privacy options need to be displayed. +/// +/// @deprecated This enum has been moved to the firebase::ump namespace. enum PrivacyOptionsRequirementStatus { /// Privacy options requirement status is unknown. Call /// `ConsentInfo::RequestConsentInfoUpdate()` to update. diff --git a/gradle.properties b/gradle.properties index ac891ac594..a2e0cd711e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ android.useAndroidX = true org.gradle.jvmargs=-Xmx2560m +org.gradle.java.home=/usr/lib/jvm/java-21-openjdk-amd64 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 78b9533282..6ea653db98 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/installations/build.gradle b/installations/build.gradle index dd60ba8050..58bde27730 100644 --- a/installations/build.gradle +++ b/installations/build.gradle @@ -85,4 +85,7 @@ apply from: "$rootDir/android_build_files/generate_proguard.gradle" project.afterEvaluate { generateProguardFile('installations') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/installations/integration_test/Podfile b/installations/integration_test/Podfile index 87c065158b..4d2c7d5ce5 100644 --- a/installations/integration_test/Podfile +++ b/installations/integration_test/Podfile @@ -5,8 +5,8 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Analytics', '11.10.0' - pod 'Firebase/Installations', '11.10.0' + pod 'Firebase/Analytics', '11.14.0' + pod 'Firebase/Installations', '11.14.0' end post_install do |installer| diff --git a/installations/integration_test/build.gradle b/installations/integration_test/build.gradle index cc000277c8..6da58f4670 100644 --- a/installations/integration_test/build.gradle +++ b/installations/integration_test/build.gradle @@ -96,3 +96,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/ios_pod/Podfile b/ios_pod/Podfile index 33f6c54d0c..f81729bd8a 100644 --- a/ios_pod/Podfile +++ b/ios_pod/Podfile @@ -3,22 +3,22 @@ platform :ios, '13.0' use_frameworks! target 'GetPods' do - pod 'Firebase/Core', '11.10.0' + pod 'Firebase/Core', '11.14.0' pod 'Google-Mobile-Ads-SDK', '11.2.0' pod 'GoogleUserMessagingPlatform', '2.3.0' - pod 'Firebase/Analytics', '11.10.0' - pod 'Firebase/AppCheck', '11.10.0' - pod 'Firebase/Auth', '11.10.0' - pod 'Firebase/Crashlytics', '11.10.0' - pod 'Firebase/Database', '11.10.0' - pod 'Firebase/DynamicLinks', '11.10.0' - pod 'Firebase/Firestore', '11.10.0' - pod 'Firebase/Functions', '11.10.0' - pod 'Firebase/Installations', '11.10.0' - pod 'Firebase/Messaging', '11.10.0' - pod 'Firebase/RemoteConfig', '11.10.0' - pod 'Firebase/Storage', '11.10.0' + pod 'Firebase/Analytics', '11.14.0' + pod 'Firebase/AppCheck', '11.14.0' + pod 'Firebase/Auth', '11.14.0' + pod 'Firebase/Crashlytics', '11.14.0' + pod 'Firebase/Database', '11.14.0' + pod 'Firebase/DynamicLinks', '11.14.0' + pod 'Firebase/Firestore', '11.14.0' + pod 'Firebase/Functions', '11.14.0' + pod 'Firebase/Installations', '11.14.0' + pod 'Firebase/Messaging', '11.14.0' + pod 'Firebase/RemoteConfig', '11.14.0' + pod 'Firebase/Storage', '11.14.0' end diff --git a/ios_pod/swift_headers/FirebaseAnalytics-Swift.h b/ios_pod/swift_headers/FirebaseAnalytics-Swift.h index 9ebd2732c2..a5d9d732a5 100644 --- a/ios_pod/swift_headers/FirebaseAnalytics-Swift.h +++ b/ios_pod/swift_headers/FirebaseAnalytics-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEANALYTICS_SWIFT_H #define FIREBASEANALYTICS_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -338,10 +342,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEANALYTICS_SWIFT_H #define FIREBASEANALYTICS_SWIFT_H #pragma clang diagnostic push @@ -384,6 +388,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -397,6 +403,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -649,6 +656,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseAuth-Swift.h b/ios_pod/swift_headers/FirebaseAuth-Swift.h index c9967d4be2..c618f1c542 100644 --- a/ios_pod/swift_headers/FirebaseAuth-Swift.h +++ b/ios_pod/swift_headers/FirebaseAuth-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEAUTH_SWIFT_H #define FIREBASEAUTH_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -316,6 +319,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -3079,10 +3083,10 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEAUTH_SWIFT_H #define FIREBASEAUTH_SWIFT_H #pragma clang diagnostic push @@ -3125,6 +3129,8 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -3138,6 +3144,7 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -3394,6 +3401,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseCoreInternal-Swift.h b/ios_pod/swift_headers/FirebaseCoreInternal-Swift.h index 32d5e080bc..3f61929f19 100644 --- a/ios_pod/swift_headers/FirebaseCoreInternal-Swift.h +++ b/ios_pod/swift_headers/FirebaseCoreInternal-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASECOREINTERNAL_SWIFT_H #define FIREBASECOREINTERNAL_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -313,6 +316,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -402,10 +406,10 @@ SWIFT_CLASS_NAMED("_ObjC_HeartbeatsPayload") #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASECOREINTERNAL_SWIFT_H #define FIREBASECOREINTERNAL_SWIFT_H #pragma clang diagnostic push @@ -448,6 +452,8 @@ SWIFT_CLASS_NAMED("_ObjC_HeartbeatsPayload") #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -461,6 +467,7 @@ SWIFT_CLASS_NAMED("_ObjC_HeartbeatsPayload") #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -714,6 +721,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseDatabase-Swift.h b/ios_pod/swift_headers/FirebaseDatabase-Swift.h index 1bf05ddf62..91bc248188 100644 --- a/ios_pod/swift_headers/FirebaseDatabase-Swift.h +++ b/ios_pod/swift_headers/FirebaseDatabase-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEDATABASE_SWIFT_H #define FIREBASEDATABASE_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -338,10 +342,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEDATABASE_SWIFT_H #define FIREBASEDATABASE_SWIFT_H #pragma clang diagnostic push @@ -384,6 +388,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -397,6 +403,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -649,6 +656,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseFirestore-Swift.h b/ios_pod/swift_headers/FirebaseFirestore-Swift.h index 2b83898d70..9d64283e55 100644 --- a/ios_pod/swift_headers/FirebaseFirestore-Swift.h +++ b/ios_pod/swift_headers/FirebaseFirestore-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEFIRESTORE_SWIFT_H #define FIREBASEFIRESTORE_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -313,6 +316,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -346,10 +350,10 @@ SWIFT_CLASS_PROPERTY(@property(nonatomic, class, readonly, #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEFIRESTORE_SWIFT_H #define FIREBASEFIRESTORE_SWIFT_H #pragma clang diagnostic push @@ -392,6 +396,8 @@ SWIFT_CLASS_PROPERTY(@property(nonatomic, class, readonly, #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -405,6 +411,7 @@ SWIFT_CLASS_PROPERTY(@property(nonatomic, class, readonly, #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -658,6 +665,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseFunctions-Swift.h b/ios_pod/swift_headers/FirebaseFunctions-Swift.h index e1918defa2..d27059ef99 100644 --- a/ios_pod/swift_headers/FirebaseFunctions-Swift.h +++ b/ios_pod/swift_headers/FirebaseFunctions-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEFUNCTIONS_SWIFT_H #define FIREBASEFUNCTIONS_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -314,6 +317,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -622,10 +626,10 @@ SWIFT_CLASS_NAMED("HTTPSCallableResult") #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEFUNCTIONS_SWIFT_H #define FIREBASEFUNCTIONS_SWIFT_H #pragma clang diagnostic push @@ -668,6 +672,8 @@ SWIFT_CLASS_NAMED("HTTPSCallableResult") #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -681,6 +687,7 @@ SWIFT_CLASS_NAMED("HTTPSCallableResult") #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -935,6 +942,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseInAppMessaging-Swift.h b/ios_pod/swift_headers/FirebaseInAppMessaging-Swift.h index 37914ebfaf..7c10bac246 100644 --- a/ios_pod/swift_headers/FirebaseInAppMessaging-Swift.h +++ b/ios_pod/swift_headers/FirebaseInAppMessaging-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEINAPPMESSAGING_SWIFT_H #define FIREBASEINAPPMESSAGING_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -337,10 +341,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEINAPPMESSAGING_SWIFT_H #define FIREBASEINAPPMESSAGING_SWIFT_H #pragma clang diagnostic push @@ -383,6 +387,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -396,6 +402,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -648,6 +655,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseMLModelDownloader-Swift.h b/ios_pod/swift_headers/FirebaseMLModelDownloader-Swift.h index 6e9df1b823..f05775a241 100644 --- a/ios_pod/swift_headers/FirebaseMLModelDownloader-Swift.h +++ b/ios_pod/swift_headers/FirebaseMLModelDownloader-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEMLMODELDOWNLOADER_SWIFT_H #define FIREBASEMLMODELDOWNLOADER_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -338,10 +342,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEMLMODELDOWNLOADER_SWIFT_H #define FIREBASEMLMODELDOWNLOADER_SWIFT_H #pragma clang diagnostic push @@ -384,6 +388,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -397,6 +403,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -649,6 +656,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseRemoteConfig-Swift.h b/ios_pod/swift_headers/FirebaseRemoteConfig-Swift.h index 4f1ae031af..778efc0084 100644 --- a/ios_pod/swift_headers/FirebaseRemoteConfig-Swift.h +++ b/ios_pod/swift_headers/FirebaseRemoteConfig-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEREMOTECONFIG_SWIFT_H #define FIREBASEREMOTECONFIG_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -338,10 +342,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASEREMOTECONFIG_SWIFT_H #define FIREBASEREMOTECONFIG_SWIFT_H #pragma clang diagnostic push @@ -384,6 +388,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -397,6 +403,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -649,6 +656,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseSessions-Swift.h b/ios_pod/swift_headers/FirebaseSessions-Swift.h index 350580fc11..fef5f2f3ce 100644 --- a/ios_pod/swift_headers/FirebaseSessions-Swift.h +++ b/ios_pod/swift_headers/FirebaseSessions-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESESSIONS_SWIFT_H #define FIREBASESESSIONS_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -313,6 +316,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -341,6 +345,12 @@ SWIFT_CLASS_NAMED("SessionDetails") enum FIRSessionsSubscriberName : NSInteger; +/// Sessions Dependencies determines when a dependent SDK is +/// installed in the app. The Sessions SDK uses this to figure +/// out which dependencies to wait for to getting the data +/// collection state. +/// This is important because the Sessions SDK starts up before +/// dependent SDKs SWIFT_CLASS_NAMED("SessionsDependencies") @interface FIRSessionsDependencies : NSObject + (void)addDependencyWithName:(enum FIRSessionsSubscriberName)name; @@ -383,10 +393,10 @@ typedef SWIFT_ENUM_NAMED(NSInteger, FIRSessionsSubscriberName, #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESESSIONS_SWIFT_H #define FIREBASESESSIONS_SWIFT_H #pragma clang diagnostic push @@ -429,6 +439,8 @@ typedef SWIFT_ENUM_NAMED(NSInteger, FIRSessionsSubscriberName, #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -442,6 +454,7 @@ typedef SWIFT_ENUM_NAMED(NSInteger, FIRSessionsSubscriberName, #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -695,6 +708,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -723,6 +737,12 @@ SWIFT_CLASS_NAMED("SessionDetails") enum FIRSessionsSubscriberName : NSInteger; +/// Sessions Dependencies determines when a dependent SDK is +/// installed in the app. The Sessions SDK uses this to figure +/// out which dependencies to wait for to getting the data +/// collection state. +/// This is important because the Sessions SDK starts up before +/// dependent SDKs SWIFT_CLASS_NAMED("SessionsDependencies") @interface FIRSessionsDependencies : NSObject + (void)addDependencyWithName:(enum FIRSessionsSubscriberName)name; diff --git a/ios_pod/swift_headers/FirebaseSharedSwift-Swift.h b/ios_pod/swift_headers/FirebaseSharedSwift-Swift.h index d5d27acd45..19931c1383 100644 --- a/ios_pod/swift_headers/FirebaseSharedSwift-Swift.h +++ b/ios_pod/swift_headers/FirebaseSharedSwift-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESHAREDSWIFT_SWIFT_H #define FIREBASESHAREDSWIFT_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -337,10 +341,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESHAREDSWIFT_SWIFT_H #define FIREBASESHAREDSWIFT_SWIFT_H #pragma clang diagnostic push @@ -383,6 +387,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -396,6 +402,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -648,6 +655,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/FirebaseStorage-Swift.h b/ios_pod/swift_headers/FirebaseStorage-Swift.h index df5f8b9344..2656ab75eb 100644 --- a/ios_pod/swift_headers/FirebaseStorage-Swift.h +++ b/ios_pod/swift_headers/FirebaseStorage-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESTORAGE_SWIFT_H #define FIREBASESTORAGE_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -315,6 +318,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -1059,10 +1063,10 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef FIREBASESTORAGE_SWIFT_H #define FIREBASESTORAGE_SWIFT_H #pragma clang diagnostic push @@ -1105,6 +1109,8 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -1118,6 +1124,7 @@ SWIFT_AVAILABILITY(watchos, introduced = 7) #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -1373,6 +1380,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/Promises-Swift.h b/ios_pod/swift_headers/Promises-Swift.h index f36ceea7a4..0eff122337 100644 --- a/ios_pod/swift_headers/Promises-Swift.h +++ b/ios_pod/swift_headers/Promises-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef PROMISES_SWIFT_H #define PROMISES_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -336,10 +340,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef PROMISES_SWIFT_H #define PROMISES_SWIFT_H #pragma clang diagnostic push @@ -382,6 +386,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -395,6 +401,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -647,6 +654,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/ios_pod/swift_headers/SwiftProtobuf-Swift.h b/ios_pod/swift_headers/SwiftProtobuf-Swift.h index b61db7e0af..6f3f9d16e2 100644 --- a/ios_pod/swift_headers/SwiftProtobuf-Swift.h +++ b/ios_pod/swift_headers/SwiftProtobuf-Swift.h @@ -1,10 +1,10 @@ #if 0 #elif defined(__arm64__) && __arm64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef SWIFTPROTOBUF_SWIFT_H #define SWIFTPROTOBUF_SWIFT_H #pragma clang diagnostic push @@ -47,6 +47,8 @@ #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -60,6 +62,7 @@ #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -312,6 +315,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") @@ -337,10 +341,10 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #elif defined(__x86_64__) && __x86_64__ // Copyright 2025 Google LLC -// Copied from Firebase iOS SDK 11.10.0. +// Copied from Firebase iOS SDK 11.14.0. -// Generated by Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 -// clang-1500.1.0.2.5) +// Generated by Apple Swift version 6.0.3 effective-5.10 (swiftlang-6.0.3.1.10 +// clang-1600.0.30.1) #ifndef SWIFTPROTOBUF_SWIFT_H #define SWIFTPROTOBUF_SWIFT_H #pragma clang diagnostic push @@ -383,6 +387,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #include #endif #if defined(__cplusplus) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" #if defined(__arm64e__) && __has_include() #include #else @@ -396,6 +402,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #endif #pragma clang diagnostic pop #endif +#pragma clang diagnostic pop #endif #if !defined(SWIFT_TYPEDEFS) @@ -648,6 +655,7 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wunknown-pragmas" #pragma clang diagnostic ignored "-Wnullability" #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension" +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #if __has_attribute(external_source_symbol) #pragma push_macro("any") diff --git a/messaging/build.gradle b/messaging/build.gradle index 3bf197d932..01c6f0ad05 100644 --- a/messaging/build.gradle +++ b/messaging/build.gradle @@ -87,4 +87,7 @@ apply from: "$rootDir/android_build_files/generate_proguard.gradle" project.afterEvaluate { generateProguardFile('messaging') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/messaging/integration_test/Podfile b/messaging/integration_test/Podfile index 27b397c85a..c09e398fa6 100644 --- a/messaging/integration_test/Podfile +++ b/messaging/integration_test/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Messaging', '11.10.0' - pod 'Firebase/Functions', '11.10.0' + pod 'Firebase/Messaging', '11.14.0' + pod 'Firebase/Functions', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Messaging', '11.10.0' - pod 'Firebase/Functions', '11.10.0' + pod 'Firebase/Messaging', '11.14.0' + pod 'Firebase/Functions', '11.14.0' end post_install do |installer| diff --git a/messaging/integration_test/build.gradle b/messaging/integration_test/build.gradle index 67f39bf793..a9f76da475 100644 --- a/messaging/integration_test/build.gradle +++ b/messaging/integration_test/build.gradle @@ -101,3 +101,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/messaging/messaging_java/build.gradle b/messaging/messaging_java/build.gradle index 03e5bb2e2f..b039c111b4 100644 --- a/messaging/messaging_java/build.gradle +++ b/messaging/messaging_java/build.gradle @@ -61,7 +61,7 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-messaging' implementation 'com.google.flatbuffers:flatbuffers-java:1.12.0' diff --git a/messaging/src/android/cpp/messaging.cc b/messaging/src/android/cpp/messaging.cc index 43c8035d71..0a0b3671ee 100644 --- a/messaging/src/android/cpp/messaging.cc +++ b/messaging/src/android/cpp/messaging.cc @@ -724,6 +724,11 @@ static void InstallationsGetToken() { result.OnCompletion( [](const Future& result, void* voidptr) { + if (g_registration_token_mutex) { + MutexLock lock(*g_registration_token_mutex); + g_registration_token_received = true; + HandlePendingSubscriptions(); + } NotifyListenerOnTokenReceived(result.result()->c_str()); }, nullptr); @@ -868,6 +873,9 @@ static const char kErrorMessageNoRegistrationToken[] = "Cannot update subscription when SetTokenRegistrationOnInitEnabled is set " "to false."; +static const char kErrorMessageSubscriptionUnknown[] = + "Cannot update subscription for unknown reason."; + Future Subscribe(const char* topic) { FIREBASE_ASSERT_MESSAGE_RETURN(Future(), internal::IsInitialized(), kMessagingNotInitializedError); @@ -882,6 +890,11 @@ Future Subscribe(const char* topic) { kErrorMessageNoRegistrationToken); } else if (g_pending_subscriptions) { g_pending_subscriptions->push_back(PendingTopic(topic, handle)); + } else { + // This shouldn't happen, since g_pending_subscriptions should be valid if + // here, but handle it to prevent abandoning the Future in case something + // happens. + api->Complete(handle, kErrorUnknown, kErrorMessageSubscriptionUnknown); } return MakeFuture(api, handle); } @@ -907,6 +920,11 @@ Future Unsubscribe(const char* topic) { kErrorMessageNoRegistrationToken); } else if (g_pending_unsubscriptions) { g_pending_unsubscriptions->push_back(PendingTopic(topic, handle)); + } else { + // This shouldn't happen, since g_pending_unsubscriptions should be valid if + // here, but handle it to prevent abandoning the Future in case something + // happens. + api->Complete(handle, kErrorUnknown, kErrorMessageSubscriptionUnknown); } return MakeFuture(api, handle); } diff --git a/messaging/src/ios/messaging.mm b/messaging/src/ios/messaging.mm index 3029bc21c0..680a29011e 100644 --- a/messaging/src/ios/messaging.mm +++ b/messaging/src/ios/messaging.mm @@ -870,7 +870,7 @@ @implementation UIApplication (FIRFCM) + (void)load { // C++ constructors may not be called yet so call NSLog rather than LogInfo. NSLog(@"FCM: Loading UIApplication FIRFCM category"); - ::firebase::util::ForEachAppDelegateClass(^(Class clazz) { + ::firebase::util::RunOnAppDelegateClasses(^(Class clazz) { FirebaseMessagingHookAppDelegate(clazz); }); } diff --git a/release_build_files/Android/firebase_dependencies.gradle b/release_build_files/Android/firebase_dependencies.gradle index ede4076af2..c9d1fc0584 100644 --- a/release_build_files/Android/firebase_dependencies.gradle +++ b/release_build_files/Android/firebase_dependencies.gradle @@ -20,15 +20,13 @@ def firebaseDependenciesMap = [ 'app_check' : ['com.google.firebase:firebase-appcheck', 'com.google.firebase:firebase-appcheck-debug', 'com.google.firebase:firebase-appcheck-playintegrity'], - 'play_services' : ['com.google.android.gms:play-services-base:18.6.0'], + 'play_services' : ['com.google.android.gms:play-services-base:18.7.0'], 'analytics' : ['com.google.firebase:firebase-analytics'], 'auth' : ['com.google.firebase:firebase-auth'], 'database' : ['com.google.firebase:firebase-database'], 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links'], 'firestore' : ['com.google.firebase:firebase-firestore'], 'functions' : ['com.google.firebase:firebase-functions'], - 'gma' : ['com.google.android.gms:play-services-ads:23.0.0', - 'com.google.android.ump:user-messaging-platform:2.2.0'], 'installations' : ['com.google.firebase:firebase-installations'], 'invites' : ['com.google.firebase:firebase-invites'], // Messaging has an additional local dependency to include. @@ -39,7 +37,8 @@ def firebaseDependenciesMap = [ 'performance' : ['com.google.firebase:firebase-perf'], 'remote_config' : ['com.google.firebase:firebase-config'], 'storage' : ['com.google.firebase:firebase-storage'], - 'testlab' : [] + 'testlab' : [], + 'ump' : ['com.google.android.ump:user-messaging-platform:2.2.0'] ] // Handles adding the Firebase C++ dependencies as properties. @@ -74,9 +73,6 @@ class Dependencies { def getFunctions() { libSet.add('functions') } - def getGma() { - libSet.add('gma') - } def getInstallations() { libSet.add('installations') } @@ -98,6 +94,9 @@ class Dependencies { def getTestlab() { libSet.add('testlab') } + def getUmp() { + libSet.add('ump') + } } // Extension to handle which Firebase C++ dependencies are being added to the @@ -132,7 +131,7 @@ project.afterEvaluate { // Add the bill-of-materials project.dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') } for (String lib : firebaseCpp.dependencies.libSet) { // Messaging includes an aar, which to be depended on properly requires diff --git a/release_build_files/CMakeLists.txt b/release_build_files/CMakeLists.txt index e9e341e0aa..1d7f4c692c 100644 --- a/release_build_files/CMakeLists.txt +++ b/release_build_files/CMakeLists.txt @@ -96,13 +96,13 @@ add_firebase_target(firebase_database) add_firebase_target(firebase_dynamic_links) add_firebase_target(firebase_firestore) add_firebase_target(firebase_functions) -add_firebase_target(firebase_gma) add_firebase_target(firebase_installations) add_firebase_target(firebase_messaging) add_firebase_target(firebase_performance) add_firebase_target(firebase_remote_config) add_firebase_target(firebase_storage) add_firebase_target(firebase_testlab) +add_firebase_target(firebase_ump) # Auth on Linux desktop has an additional dependency on libsecret, # which needs to be added. If it cannot be found, we don't want to diff --git a/release_build_files/readme.md b/release_build_files/readme.md index ae6d754b2c..68af3b159b 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -10,12 +10,13 @@ on *iOS* and *Android*: * Firebase Dynamic Links (deprecated SDK) * Cloud Firestore * Firebase Functions -* Google Mobile Ads (with User Messaging Platform) +* Google Mobile Ads (deprecated SDK) * Firebase Installations * Firebase Instance ID (deprecated SDK) * Firebase Realtime Database * Firebase Remote Config * Firebase Storage +* User Messaging Platform ## Desktop Workflow Implementations @@ -86,7 +87,7 @@ distributed as part of the core Firebase Feature | Required Libraries and Gradle Packages -------------------------- | -------------------------------------- -All Firebase SDKs | platform(com.google.firebase:firebase-bom:33.11.0) +All Firebase SDKs | platform(com.google.firebase:firebase-bom:33.15.0) | | (Android Bill of Materials) Firebase Analytics | libfirebase_analytics.a | | libfirebase_app.a @@ -149,7 +150,7 @@ Firebase Messaging | libfirebase_messaging.a | | com.google.firebase:firebase-messaging | | (Maven package) | | libmessaging_java.jar (Android service) -| | androidx.core:core:1.15.0 (Maven package) +| | androidx.core:core:1.16.0 (Maven package) Firebase Realtime Database | libfirebase_database.a | | libfirebase_auth.a | | libfirebase_app.a @@ -174,7 +175,13 @@ Firebase Storage | libfirebase_storage.a | | (Maven package) | | com.google.firebase:firebase-auth | | (Maven package) -Google Play services module| com.google.android.gms:play-services-base:18.6.0 +User Messaging Platform | libfirebase_ump.a +| | libfirebase_app.a +| | com.google.firebase:firebase-analytics +| | (Maven package) +| | com.google.android.ump:user-messaging-platform:2.2.0 +| | (Maven package) +Google Play services module| com.google.android.gms:play-services-base:18.7.0 | | (Maven package) The Firebase C++ SDK uses an Android BoM (Bill of Materials) to specify a single @@ -206,6 +213,7 @@ firebaseCpp.dependencies { messaging remoteConfig storage + ump } ``` @@ -230,50 +238,54 @@ Feature | Required Frameworks and Cocoapods -------------------------- | --------------------------------------- Firebase Analytics | firebase_analytics.xcframework | | firebase.xcframework -| | Firebase/Analytics Cocoapod (11.10.0) +| | Firebase/Analytics Cocoapod (11.14.0) Firebase App Check | firebase_app_check.xcframework | | firebase.xcframework -| | Firebase/AppCheck Cocoapod (11.10.0) +| | Firebase/AppCheck Cocoapod (11.14.0) Firebase Authentication | firebase_auth.xcframework | | firebase.xcframework -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Dynamic Links | firebase_dynamic_links.xcframework | | firebase.xcframework -| | Firebase/DynamicLinks Cocoapod (11.10.0) +| | Firebase/DynamicLinks Cocoapod (11.14.0) Cloud Firestore | firebase_firestore.xcframework | | firebase_auth.xcframework | | firebase.xcframework -| | Firebase/Firestore Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Firestore Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Functions | firebase_functions.xcframework | | firebase_auth.xcframework (optional) | | firebase.xcframework -| | Firebase/Functions Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Functions Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Google Mobile Ads | firebase_gma.xcframework | | firebase.xcframework -| | Firebase/CoreOnly Cocoapod (11.10.0) +| | Firebase/CoreOnly Cocoapod (11.14.0) | | Google-Mobile-Ads-SDK Cocoapod (11.2.0) | | GoogleUserMessagingPlatform Cocoapod (2.3.0) Firebase Installations | firebase_installations.xcframework | | firebase.xcframework -| | FirebaseInstallations Cocoapod (11.10.0) +| | FirebaseInstallations Cocoapod (11.14.0) Firebase Cloud Messaging | firebase_messaging.xcframework | | firebase.xcframework -| | Firebase/Messaging Cocoapod (11.10.0) +| | Firebase/Messaging Cocoapod (11.14.0) Firebase Realtime Database | firebase_database.xcframework | | firebase_auth.xcframework | | firebase.xcframework -| | Firebase/Database Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Database Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Remote Config | firebase_remote_config.xcframework | | firebase.xcframework -| | Firebase/RemoteConfig Cocoapod (11.10.0) +| | Firebase/RemoteConfig Cocoapod (11.14.0) Firebase Storage | firebase_storage.xcframework | | firebase_auth.xcframework | | firebase.xcframework -| | Firebase/Storage Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Storage Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) +User Messaging Platform | firebase_ump.xcframework +| | firebase.xcframework +| | Firebase/CoreOnly Cocoapod (11.14.0) +| | GoogleUserMessagingPlatform Cocoapod (2.3.0) Important: Each version of the Firebase C++ SDK supports a specific version of the Firebase iOS SDK. Please ensure that you reference the Cocoapod versions @@ -293,50 +305,54 @@ Feature | Required Libraries and Cocoapods -------------------------- | ----------------------------------------- Firebase Analytics | libfirebase_analytics.a | | libfirebase_app.a -| | Firebase/Analytics Cocoapod (11.10.0) +| | Firebase/Analytics Cocoapod (11.14.0) Firebase App Check | firebase_app_check.xcframework | | firebase.xcframework -| | Firebase/AppCheck Cocoapod (11.10.0) +| | Firebase/AppCheck Cocoapod (11.14.0) Firebase Authentication | libfirebase_auth.a | | libfirebase_app.a -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Dynamic Links | libfirebase_dynamic_links.a | | libfirebase_app.a -| | Firebase/DynamicLinks Cocoapod (11.10.0) +| | Firebase/DynamicLinks Cocoapod (11.14.0) Cloud Firestore | libfirebase_firestore.a | | libfirebase_app.a | | libfirebase_auth.a -| | Firebase/Firestore Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Firestore Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Functions | libfirebase_functions.a | | libfirebase_app.a | | libfirebase_auth.a (optional) -| | Firebase/Functions Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Functions Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Google Mobile Ads | libfirebase_gma.a | | libfirebase_app.a -| | Firebase/CoreOnly Cocoapod (11.10.0) +| | Firebase/CoreOnly Cocoapod (11.14.0) | | Google-Mobile-Ads-SDK Cocoapod (11.2.0) | | GoogleUserMessagingPlatform Cocoapod (2.3.0) Firebase Installations | libfirebase_installations.a | | libfirebase_app.a -| | FirebaseInstallations Cocoapod (11.10.0) +| | FirebaseInstallations Cocoapod (11.14.0) Firebase Cloud Messaging | libfirebase_messaging.a | | libfirebase_app.a -| | Firebase/CloudMessaging Cocoapod (11.10.0) +| | Firebase/CloudMessaging Cocoapod (11.14.0) Firebase Realtime Database | libfirebase_database.a | | libfirebase_app.a | | libfirebase_auth.a -| | Firebase/Database Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Database Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) Firebase Remote Config | libfirebase_remote_config.a | | libfirebase_app.a -| | Firebase/RemoteConfig Cocoapod (11.10.0) +| | Firebase/RemoteConfig Cocoapod (11.14.0) Firebase Storage | libfirebase_storage.a | | libfirebase_app.a | | libfirebase_auth.a -| | Firebase/Storage Cocoapod (11.10.0) -| | Firebase/Auth Cocoapod (11.10.0) +| | Firebase/Storage Cocoapod (11.14.0) +| | Firebase/Auth Cocoapod (11.14.0) +User Messaging Platform | libfirebase_ump.a +| | libfirebase_app.a +| | Firebase/CoreOnly Cocoapod (11.14.0) +| | GoogleUserMessagingPlatform Cocoapod (2.3.0) Important: Each version of the Firebase C++ SDK supports a specific version of the Firebase iOS SDK. Please ensure that you reference the Cocoapod versions @@ -387,6 +403,8 @@ Firebase Installations (stub) | libfirebase_installations.a | | libfirebase_app.a Firebase Cloud Messaging (stub) | libfirebase_messaging.a | | libfirebase_app.a +User Messaging Platform (stub) | libfirebase_ump.a +| | libfirebase_app.a The provided libraries have been tested using GCC 4.8.0, GCC 7.2.0, and Clang 5.0 on Ubuntu. When building C++ desktop apps on Linux, you will need to link @@ -430,8 +448,10 @@ Firebase Installations (stub) | firebase_installations.framework | | firebase.framework Firebase Cloud Messaging (stub) | firebase_messaging.framework | | firebase.framework +User Messaging Platform (stub) | libfirebase_ump.a +| | libfirebase_app.a -The provided libraries have been tested using Xcode 14.1. When building C++ +The provided libraries have been tested using Xcode 16.2. When building C++ desktop apps on OS X, you will need to link the `gssapi_krb5` and `pthread` system libraries, as well as the `CoreFoundation`, `Foundation`, `GSS`, and `Security` OS X system frameworks (consult your compiler documentation for more @@ -474,6 +494,8 @@ Firebase Installations (stub) | firebase_installations.lib | | firebase_app.lib Firebase Cloud Messaging (stub) | firebase_messaging.lib | | firebase_app.lib +User Messaging Platform (stub) | firebase_ump.lib +| | firebase_app.lib The provided libraries have been tested using Visual Studio 2019. When building C++ desktop apps on Windows, you will need to link the following @@ -515,9 +537,30 @@ addition to any you may have implemented. The Firebase Cloud Messaging library needs to attach handlers to the application delegate using method swizzling. If you are using -these libraries, at load time, Firebase will identify your `AppDelegate` class -and swizzle the required methods onto it, chaining a call back to your existing -method implementation. +these libraries, at load time, Firebase will typically identify your `AppDelegate` +class and swizzle the required methods onto it. + +#### Specifying Your AppDelegate Class Directly (iOS) + +For a more direct approach, or if you encounter issues with the default +method swizzling, you can explicitly tell Firebase which class is your +application's `AppDelegate`. To do this, add the `FirebaseAppDelegateClassName` +key to your app's `Info.plist` file: + +* **Key:** `FirebaseAppDelegateClassName` +* **Type:** `String` +* **Value:** Your AppDelegate's class name (e.g., `MyCustomAppDelegate`) + +**Example `Info.plist` entry:** +```xml +FirebaseAppDelegateClassName +MyCustomAppDelegate +``` + +If this key is provided with a valid class name, Firebase will use that class +directly for its AppDelegate-related interactions. If the key is not present, +is invalid, or the class is not found, Firebase will use its standard method +swizzling approach. ### Custom Android Build Systems @@ -577,21 +620,22 @@ initialization status. These can be used without Google Play services. The table below summarizes whether Google Play services is required by each Firebase C++ library. -Firebase C++ Library | Google Play services required? --------------------- | --------------------------------- -Analytics | Not required -App Check | Not required -Cloud Messaging | Required -Auth | Required -Dynamic Links | Required -Firestore | Required -Functions | Required -Installations | Not Required -Instance ID | Required -Google Mobile Ads | Not required (usually; see below) -Realtime Database | Required -Remote Config | Required -Storage | Required +Firebase C++ Library | Google Play services required? +------------------------ | --------------------------------- +Analytics | Not required +App Check | Not required +Cloud Messaging | Required +Auth | Required +Dynamic Links | Required +Firestore | Required +Functions | Required +Installations | Not Required +Instance ID | Required +Google Mobile Ads | Not required (usually; see below) +Realtime Database | Required +Remote Config | Required +Storage | Required +User Messaging Platform | Not required #### A note on Google Mobile Ads and Google Play services @@ -631,6 +675,28 @@ workflow use only during the development of your app, not for publicly shipping code. ## Release Notes +### Upcoming Release +- Changes + - iOS: Added an option to explicitly specify your app's `AppDelegate` class + name via the `FirebaseAppDelegateClassName` key in `Info.plist`. This + provides a more direct way for Firebase to interact with your specified + AppDelegate. See "Platform Notes > iOS Method Swizzling > + Specifying Your AppDelegate Class Directly (iOS)" for details. + - General: Removed the deprecated Google Mobile Ads (GMA) C++ SDK. + +### 12.8.0 +- Changes + - General (iOS): Update to Firebase Cocoapods version 11.14.0. + - General (Android): Update to Firebase Android BoM version 33.15.0. + - General (iOS, tvOS, Desktop): iOS, tvOS, and macOS SDKs are now built + using Xcode 16.2. + - UMP: Moved the User Messaging Platform SDK to its own top-level + library and to the firebase::ump namespace. The version in the + GMA library (in firebase::gma::ump) has been deprecated and will + be removed soon. + - Messaging (Android): Fix issue with the Subscribe Future not completing + when a cached token is available. + ### 12.7.0 - Changes - General (iOS): Update to Firebase Cocoapods version 11.10.0. diff --git a/remote_config/build.gradle b/remote_config/build.gradle index 2b00fbdaae..fe57588e18 100644 --- a/remote_config/build.gradle +++ b/remote_config/build.gradle @@ -87,4 +87,7 @@ project.afterEvaluate { generateProguardFile('remote_config') setupDexDependencies(':remote_config:remote_config_resources') preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/remote_config/integration_test/Podfile b/remote_config/integration_test/Podfile index 10607cf649..bfa3642d8a 100644 --- a/remote_config/integration_test/Podfile +++ b/remote_config/integration_test/Podfile @@ -4,12 +4,12 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/RemoteConfig', '11.10.0' + pod 'Firebase/RemoteConfig', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/RemoteConfig', '11.10.0' + pod 'Firebase/RemoteConfig', '11.14.0' end post_install do |installer| diff --git a/remote_config/integration_test/build.gradle b/remote_config/integration_test/build.gradle index 389718f1e6..31239ec5f3 100644 --- a/remote_config/integration_test/build.gradle +++ b/remote_config/integration_test/build.gradle @@ -99,3 +99,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/remote_config/remote_config_resources/build.gradle b/remote_config/remote_config_resources/build.gradle index cdf73ed30a..3dbbf34c06 100644 --- a/remote_config/remote_config_resources/build.gradle +++ b/remote_config/remote_config_resources/build.gradle @@ -55,12 +55,15 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-config' } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/scripts/dumpsrc.sh b/scripts/dumpsrc.sh new file mode 100755 index 0000000000..1ec63275b3 --- /dev/null +++ b/scripts/dumpsrc.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# 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. + +# Print a formatted list of source files from specific directories. +# Suggested usage: dumpsrc.sh [path2] [path3] [...] | pbcopy + +included_files=( + '*.swig' + '*.i' + '*.c' + '*.cc' + '*.cpp' + '*.cs' + '*.cmake' + '*.fbs' + '*.gradle' + '*.h' + '*.hh' + '*.java' + '*.js' + '*.json' + '*.md' + '*.m' + '*.mm' + 'CMakeLists.txt' + 'Podfile' +) + +get_markdown_language_for_file() { + local filename="$1" + local base="$(basename "$filename")" + local ext="${base##*.}" + if [[ "$base" == "$ext" && "$base" != .* ]]; then + ext="" # No extension + fi + + # Handle special filename case first + if [[ "$base" == "CMakeLists.txt" ]]; then + echo "cmake" + return 0 + fi + + # Main logic using case based on extension + case "$ext" in + c) echo "c" ;; + cc) echo "cpp" ;; + cs) echo "csharp" ;; + hh) echo "cpp" ;; + m) echo "objectivec" ;; + mm) echo "objectivec" ;; + sh) echo "bash" ;; + py) echo "python" ;; + md) echo "markdown" ;; + json) echo "js" ;; + h) + if grep -qE "\@interface|\#import" "$filename"; then + echo "objectivec"; + else + echo "cpp"; + fi + ;; + "") # Explicitly handle no extension (after CMakeLists.txt check) + : # Output nothing + ;; + *) # Default case for any other non-empty extension + echo "$ext" + ;; + esac + return 0 +} + + +if [[ -z "$1" ]]; then + echo "Usage: $0 [path2] [path3] ..." + exit 1 +fi + +find_cmd_args=('-name' 'UNUSED') + +for pattern in "${included_files[@]}"; do + if [[ -n "${pattern}" ]]; then + find_cmd_args+=('-or' '-name' "$pattern") + fi +done + +for f in `find $* -type f -and "${find_cmd_args[@]}"`; do + echo "*** BEGIN CONTENTS OF FILE '$f' ***"; + echo '```'$(get_markdown_language_for_file "$f"); + cat "$f"; + echo '```'; + echo "*** END CONTENTS OF FILE '$f' ***"; + echo +done diff --git a/scripts/gha-encrypted/ump/GoogleService-Info.plist.gpg b/scripts/gha-encrypted/ump/GoogleService-Info.plist.gpg new file mode 100644 index 0000000000..65b703ce7e Binary files /dev/null and b/scripts/gha-encrypted/ump/GoogleService-Info.plist.gpg differ diff --git a/scripts/gha-encrypted/ump/google-services.json.gpg b/scripts/gha-encrypted/ump/google-services.json.gpg new file mode 100644 index 0000000000..c46f596425 Binary files /dev/null and b/scripts/gha-encrypted/ump/google-services.json.gpg differ diff --git a/scripts/gha/build_ios_tvos.py b/scripts/gha/build_ios_tvos.py index d314024e58..0730fda249 100644 --- a/scripts/gha/build_ios_tvos.py +++ b/scripts/gha/build_ios_tvos.py @@ -51,9 +51,10 @@ 'supported_targets' : ('firebase_analytics', 'firebase_app_check', 'firebase_auth', 'firebase_database', 'firebase_dynamic_links', 'firebase_firestore', - 'firebase_functions', 'firebase_gma', + 'firebase_functions', 'firebase_installations', 'firebase_messaging', - 'firebase_remote_config', 'firebase_storage'), + 'firebase_remote_config', 'firebase_storage', + 'firebase_ump'), 'device': { 'architectures' : ['arm64'], 'toolchain' : 'cmake/toolchains/ios.cmake', @@ -590,9 +591,9 @@ def parse_cmdline_args(): default=( 'firebase_analytics', 'firebase_app_check', 'firebase_auth', 'firebase_database', 'firebase_dynamic_links', 'firebase_firestore', - 'firebase_functions', 'firebase_gma', + 'firebase_functions', 'firebase_installations', 'firebase_messaging', - 'firebase_remote_config', 'firebase_storage'), + 'firebase_remote_config', 'firebase_storage', 'firebase_ump'), help='List of CMake build targets') parser.add_argument('-o', '--os', nargs='+', default=('ios', 'tvos'), help='List of operating systems to build for.') diff --git a/scripts/gha/firebase_github.py b/scripts/gha/firebase_github.py index 79d17f0f40..9bc961fbd7 100644 --- a/scripts/gha/firebase_github.py +++ b/scripts/gha/firebase_github.py @@ -225,6 +225,49 @@ def get_reviews(token, pull_number): return results +def get_pull_request_review_comments(token, pull_number, since=None): + """https://docs.github.com/en/rest/pulls/comments#list-review-comments-on-a-pull-request""" + url = f'{GITHUB_API_URL}/pulls/{pull_number}/comments' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + + page = 1 + per_page = 100 + results = [] + + # Base parameters for the API request + base_params = {'per_page': per_page} + if since: + base_params['since'] = since + + while True: # Loop indefinitely until explicitly broken + current_page_params = base_params.copy() + current_page_params['page'] = page + + try: + with requests_retry_session().get(url, headers=headers, params=current_page_params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + # Log which page and if 'since' was used for clarity + logging.info("get_pull_request_review_comments: %s params %s response: %s", url, current_page_params, response) + + current_page_results = response.json() + if not current_page_results: # No more results on this page + break # Exit loop, no more comments to fetch + + results.extend(current_page_results) + + # If fewer results than per_page were returned, it's the last page + if len(current_page_results) < per_page: + break # Exit loop, this was the last page + + page += 1 # Increment page for the next iteration + + except requests.exceptions.RequestException as e: + logging.error(f"Error fetching review comments (page {page}, params: {current_page_params}) for PR {pull_number}: {e}") + break # Stop trying if there's an error + return results + + def create_workflow_dispatch(token, workflow_id, ref, inputs): """https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event""" url = f'{GITHUB_API_URL}/actions/workflows/{workflow_id}/dispatches' diff --git a/scripts/gha/get_pr_review_comments.py b/scripts/gha/get_pr_review_comments.py new file mode 100755 index 0000000000..0eae324858 --- /dev/null +++ b/scripts/gha/get_pr_review_comments.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# 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. + +"""Fetches and formats review comments from a GitHub Pull Request.""" + +import argparse +import os +import sys +import firebase_github +import datetime +from datetime import timezone, timedelta + + +def main(): + STATUS_IRRELEVANT = "[IRRELEVANT]" + STATUS_OLD = "[OLD]" + STATUS_CURRENT = "[CURRENT]" + + default_owner = firebase_github.OWNER + default_repo = firebase_github.REPO + + parser = argparse.ArgumentParser( + description="Fetch review comments from a GitHub PR and format into simple text output.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--pull_number", + type=int, + required=True, + help="Pull request number." + ) + parser.add_argument( + "--owner", + type=str, + default=default_owner, + help=f"Repository owner. Defaults to '{default_owner}'." + ) + parser.add_argument( + "--repo", + type=str, + default=default_repo, + help=f"Repository name. Defaults to '{default_repo}'." + ) + parser.add_argument( + "--token", + type=str, + default=os.environ.get("GITHUB_TOKEN"), + help="GitHub token. Can also be set via GITHUB_TOKEN env var." + ) + parser.add_argument( + "--context-lines", + type=int, + default=10, + help="Number of context lines from the diff hunk. 0 for full hunk. If > 0, shows header (if any) and last N lines of the remaining hunk. Default: 10." + ) + parser.add_argument( + "--since", + type=str, + default=None, + help="Only show comments updated at or after this ISO 8601 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ)." + ) + parser.add_argument( + "--exclude-old", + action="store_true", + default=False, + help="Exclude comments marked [OLD] (where line number has changed due to code updates but position is still valid)." + ) + parser.add_argument( + "--include-irrelevant", + action="store_true", + default=False, + help="Include comments marked [IRRELEVANT] (where GitHub can no longer anchor the comment to the diff, i.e., position is null)." + ) + + args = parser.parse_args() + + if not args.token: + sys.stderr.write("Error: GitHub token not provided. Set GITHUB_TOKEN or use --token.\n") + sys.exit(1) + + if args.owner != firebase_github.OWNER or args.repo != firebase_github.REPO: + repo_url = f"https://github.com/{args.owner}/{args.repo}" + if not firebase_github.set_repo_url(repo_url): + sys.stderr.write(f"Error: Invalid repo URL: {args.owner}/{args.repo}. Expected https://github.com/owner/repo\n") + sys.exit(1) + sys.stderr.write(f"Targeting repository: {firebase_github.OWNER}/{firebase_github.REPO}\n") + + sys.stderr.write(f"Fetching comments for PR #{args.pull_number} from {firebase_github.OWNER}/{firebase_github.REPO}...\n") + if args.since: + sys.stderr.write(f"Filtering comments updated since: {args.since}\n") + + + comments = firebase_github.get_pull_request_review_comments( + args.token, + args.pull_number, + since=args.since + ) + + if not comments: + sys.stderr.write(f"No review comments found for PR #{args.pull_number} (or matching filters), or an error occurred.\n") + return + + latest_activity_timestamp_obj = None + processed_comments_count = 0 + print("# Review Comments\n\n") + for comment in comments: + created_at_str = comment.get("created_at") + + current_pos = comment.get("position") + current_line = comment.get("line") + original_line = comment.get("original_line") + + status_text = "" + line_to_display = None + + if current_pos is None: + status_text = STATUS_IRRELEVANT + line_to_display = original_line + elif original_line is not None and current_line != original_line: + status_text = STATUS_OLD + line_to_display = current_line + else: + status_text = STATUS_CURRENT + line_to_display = current_line + + if line_to_display is None: + line_to_display = "N/A" + + if status_text == STATUS_IRRELEVANT and not args.include_irrelevant: + continue + if status_text == STATUS_OLD and args.exclude_old: + continue + + # Track latest 'updated_at' for '--since' suggestion; 'created_at' is for display. + updated_at_str = comment.get("updated_at") + if updated_at_str: # Check if updated_at_str is not None and not empty + try: + if sys.version_info < (3, 11): + dt_str_updated = updated_at_str.replace("Z", "+00:00") + else: + dt_str_updated = updated_at_str + current_comment_activity_dt = datetime.datetime.fromisoformat(dt_str_updated) + if latest_activity_timestamp_obj is None or current_comment_activity_dt > latest_activity_timestamp_obj: + latest_activity_timestamp_obj = current_comment_activity_dt + except ValueError: + sys.stderr.write(f"Warning: Could not parse updated_at timestamp: {updated_at_str}\n") + + # Get other comment details + user = comment.get("user", {}).get("login", "Unknown user") + path = comment.get("path", "N/A") + body = comment.get("body", "").strip() + + if not body: + continue + + processed_comments_count += 1 + + diff_hunk = comment.get("diff_hunk") + html_url = comment.get("html_url", "N/A") + comment_id = comment.get("id") + in_reply_to_id = comment.get("in_reply_to_id") + + print(f"## Comment by: **{user}** (ID: `{comment_id}`){f' (In Reply To: `{in_reply_to_id}`)' if in_reply_to_id else ''}\n") + if created_at_str: + print(f"* **Timestamp**: `{created_at_str}`") + print(f"* **Status**: `{status_text}`") + print(f"* **File**: `{path}`") + print(f"* **Line**: `{line_to_display}`") + print(f"* **URL**: <{html_url}>\n") + + print("\n### Context:") + print("```") # Start of Markdown code block + if diff_hunk and diff_hunk.strip(): + if args.context_lines == 0: # User wants the full hunk + print(diff_hunk) + else: # User wants N lines of context (args.context_lines > 0) + hunk_lines = diff_hunk.split('\n') + if hunk_lines and hunk_lines[0].startswith("@@ "): + print(hunk_lines[0]) + hunk_lines = hunk_lines[1:] # Modify list in place for remaining operations + + # Proceed with the (potentially modified) hunk_lines + # If hunk_lines is empty here (e.g. original hunk was only a header that was removed), + # hunk_lines[-args.context_lines:] will be [], and "\n".join([]) is "", + # so print("") will effectively print a newline. This is acceptable. + print("\n".join(hunk_lines[-args.context_lines:])) + else: # diff_hunk was None or empty + print("(No diff hunk available for this comment)") + print("```") # End of Markdown code block + + print("\n### Comment:") + print(body) + print("\n---") + + sys.stderr.write(f"\nPrinted {processed_comments_count} comments to stdout.\n") + + if latest_activity_timestamp_obj: + try: + # Ensure it's UTC before adding timedelta, then format + next_since_dt = latest_activity_timestamp_obj.astimezone(timezone.utc) + timedelta(seconds=2) + next_since_str = next_since_dt.strftime('%Y-%m-%dT%H:%M:%SZ') + + new_cmd_args = [sys.executable, sys.argv[0]] # Start with interpreter and script path + i = 1 # Start checking from actual arguments in sys.argv + while i < len(sys.argv): + if sys.argv[i] == "--since": + i += 2 # Skip --since and its value + continue + new_cmd_args.append(sys.argv[i]) + i += 1 + + new_cmd_args.extend(["--since", next_since_str]) + suggested_cmd = " ".join(new_cmd_args) + sys.stderr.write(f"\nTo get comments created after the last one in this batch, try:\n{suggested_cmd}\n") + except Exception as e: + sys.stderr.write(f"\nWarning: Could not generate next command suggestion: {e}\n") + +if __name__ == "__main__": + main() diff --git a/scripts/gha/integration_testing/build_testapps.json b/scripts/gha/integration_testing/build_testapps.json index aa57936b3d..f20b0ccbeb 100755 --- a/scripts/gha/integration_testing/build_testapps.json +++ b/scripts/gha/integration_testing/build_testapps.json @@ -99,15 +99,14 @@ "provision": "Firebase_Dev_Wildcard.mobileprovision" }, { - "name": "gma", - "full_name": "FirebaseGma", + "name": "ump", + "full_name": "FirebaseUmp", "bundle_id": "com.google.ios.admob.testapp", "ios_target": "integration_test", "tvos_target": "", - "has_uitests": true, - "testapp_path": "gma/integration_test", + "testapp_path": "ump/integration_test", "frameworks": [ - "firebase_gma.xcframework", + "firebase_ump.xcframework", "firebase.xcframework" ], "provision": "Google_Development.mobileprovision" @@ -260,7 +259,6 @@ "'dynamic_links' : ['com.google.firebase:firebase-dynamic-links:[0,)'],", "'firestore' : ['com.google.firebase:firebase-firestore:[0,)'],", "'functions' : ['com.google.firebase:firebase-functions:[0,)'],", - "'gma' : ['com.google.firebase:firebase-ads:[0,)',", "'instance_id' : ['com.google.firebase:firebase-iid:[0,)'],", "'messaging' : ['com.google.firebase.messaging.cpp:firebase_messaging_cpp@aar',", "'com.google.firebase:firebase-messaging:[0,)'],", diff --git a/scripts/gha/print_matrix_configuration.py b/scripts/gha/print_matrix_configuration.py index e41c70f5ac..e70e088509 100644 --- a/scripts/gha/print_matrix_configuration.py +++ b/scripts/gha/print_matrix_configuration.py @@ -73,35 +73,35 @@ PARAMETERS = { "desktop": { "matrix": { - "os": ["ubuntu-22.04", "macos-13"], + "os": ["ubuntu-22.04", "macos-14"], "build_type": ["Release", "Debug"], - "architecture": ["x64", "x86"], + "architecture": ["x64", "x86", "arm64"], "msvc_runtime": ["static","dynamic"], - "xcode_version": ["15.1"], - "python_version": ["3.7"], + "xcode_version": ["16.2"], + "python_version": ["3.8"], EXPANDED_KEY: { - "os": ["ubuntu-22.04", "macos-13", "windows-latest"], - "xcode_version": ["15.1"], + "os": ["ubuntu-22.04", "macos-14", "windows-latest"], + "xcode_version": ["16.2"], } } }, "android": { "matrix": { - "os": ["ubuntu-22.04", "macos-13", "windows-latest"], - "architecture": ["x64"], - "python_version": ["3.7"], + "os": ["ubuntu-22.04", "macos-14", "windows-latest"], + "architecture": ["x64", "arm64"], + "python_version": ["3.8"], EXPANDED_KEY: { - "os": ["ubuntu-22.04", "macos-13", "windows-latest"] + "os": ["ubuntu-22.04", "macos-14", "windows-latest"] } } }, "integration_tests": { "matrix": { - "os": ["ubuntu-22.04", "macos-13", "windows-latest"], + "os": ["ubuntu-22.04", "macos-14", "windows-latest"], "platform": ["Desktop", "Android", "iOS", "tvOS"], "ssl_lib": ["openssl"], "android_device": ["android_target", "emulator_ftl_target"], @@ -113,7 +113,7 @@ "msvc_runtime": ["dynamic"], "cpp_compiler_windows": ["VisualStudio2019"], "cpp_compiler_linux": ["clang-11.0"], - "xcode_version": ["15.1"], # only the first one is used + "xcode_version": ["16.2"], # only the first one is used "ndk_version": ["r22b"], "platform_version": ["28"], "build_tools_version": ["28.0.3"], @@ -134,17 +134,17 @@ } }, "config": { - "apis": "analytics,app_check,auth,database,dynamic_links,firestore,functions,gma,installations,messaging,remote_config,storage", + "apis": "analytics,app_check,auth,database,dynamic_links,firestore,functions,installations,messaging,remote_config,storage", "mobile_test_on": "real,virtual" } }, "ios": { "matrix": { - "xcode_version": ["15.1"], + "xcode_version": ["16.2"], EXPANDED_KEY: { - "xcode_version": ["15.1"] + "xcode_version": ["16.2"] } } }, @@ -205,25 +205,24 @@ "emulator_latest": [ {"type": "virtual", "image":"system-images;android-32;google_apis;x86_64"} ], "emulator_32bit": [ {"type": "virtual", "image":"system-images;android-30;google_apis;x86"} ], "ios_min": [ - # Slightly different OS versions because of limited FTL selection. - {"type": "ftl", "device": "model=iphone8,version=14.7"}, - {"type": "ftl", "device": "model=iphone11pro,version=14.7"}, - {"type": "ftl", "device": "model=iphone12pro,version=14.8"}, - ], - "ios_target": [ # Slightly different OS versions because of limited FTL selection. {"type": "ftl", "device": "model=iphone13pro,version=15.7"}, {"type": "ftl", "device": "model=iphone8,version=15.7"}, ], - "ios_latest": [ + "ios_target": [ + # Slightly different OS versions because of limited FTL selection. {"type": "ftl", "device": "model=iphone14pro,version=16.6"}, {"type": "ftl", "device": "model=iphone11pro,version=16.6"}, {"type": "ftl", "device": "model=iphone8,version=16.6"}, {"type": "ftl", "device": "model=ipad10,version=16.6"}, ], - "simulator_min": [ {"type": "virtual", "name":"iPhone 15 Pro Max", "version":"17.0.1"} ], + "ios_latest": [ + {"type": "ftl", "device": "model=iphone15,version=18.0"}, + {"type": "ftl", "device": "model=iphone15pro,version=18.0"}, + ], + "simulator_min": [ {"type": "virtual", "name":"iPhone 15 Pro Max", "version":"17.2"} ], "simulator_target": [ {"type": "virtual", "name":"iPhone 15 Pro Max", "version":"17.2"} ], - "simulator_latest": [ {"type": "virtual", "name":"iPhone 15 Plus", "version":"17.4"} ], + "simulator_latest": [ {"type": "virtual", "name":"iPhone 15 Pro", "version":"17.4"} ], "tvos_simulator": [ {"type": "virtual", "name":"Apple TV", "version":"16.1"} ], } @@ -317,9 +316,11 @@ def scan_changes_in_file(parm_key, auto_diff, path, requested_api_list): change_lines = [l for l in change_lines if len(l) > 20] changed_apis = set() for line in change_lines: - if ("Google-Mobile-Ads" in line or "GoogleUserMessagingPlatform" in line or - "play-services-ads" in line or "user-messaging-platform" in line): - changed_apis.add("gma") + if ("GoogleUserMessagingPlatform" in line or + "user-messaging-platform" in line): + # This is for UMP, not GMA, but since they were bundled, keep this check. + # If UMP is removed in the future, this can be removed too. + changed_apis.add("ump") else: changed_apis.update(requested_api_list) break diff --git a/scripts/gha/report_build_status.py b/scripts/gha/report_build_status.py index 38a280da46..3a7afaf202 100644 --- a/scripts/gha/report_build_status.py +++ b/scripts/gha/report_build_status.py @@ -191,7 +191,7 @@ def format_errors(all_errors, severity, event): if product == 'missing_log': product_name = 'missing logs' - elif product == 'gma': + elif product == 'ump': product_name = product.upper() else: product_name = product.replace('_', ' ').title() @@ -664,6 +664,7 @@ def main(argv): latest = latest.replace(" ", " ") product = product.replace("_", " ") product = product.upper() if product == "gma" else product.title() + product = product.upper() if product == "ump" else product.title() if len(test_list[test_id]['links']) > 0: latest = "[%s](%s)" % (latest, test_list[test_id]['links'][-1]) diff --git a/scripts/gha/utils.py b/scripts/gha/utils.py index 0bddfc7bff..d75a516580 100644 --- a/scripts/gha/utils.py +++ b/scripts/gha/utils.py @@ -19,7 +19,7 @@ platforms. """ -import distutils.spawn +import shutil import glob import platform import shutil @@ -63,7 +63,7 @@ def run_command(cmd, capture_output=False, cwd=None, check=False, as_root=False, def is_command_installed(tool): """Check if a command is installed on the system.""" - return distutils.spawn.find_executable(tool) + return shutil.which(tool) def glob_exists(glob_path): diff --git a/scripts/print_github_reviews.py b/scripts/print_github_reviews.py new file mode 100755 index 0000000000..33bebdf14a --- /dev/null +++ b/scripts/print_github_reviews.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 +# 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. + +"""Fetches and formats review comments from a GitHub Pull Request.""" + +import argparse +import os +import sys +import datetime +from datetime import timezone, timedelta +import requests +import json +import re +import subprocess +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +# Constants for GitHub API interaction +RETRIES = 3 +BACKOFF = 5 +RETRY_STATUS = (403, 500, 502, 504) # HTTP status codes to retry on +TIMEOUT = 5 # Default timeout for requests in seconds + +# Global variables for the target repository, populated by set_repo_url_standalone() +OWNER = '' +REPO = '' +BASE_URL = 'https://api.github.com' +GITHUB_API_URL = '' + + +def set_repo_url_standalone(owner_name, repo_name): + global OWNER, REPO, GITHUB_API_URL + OWNER = owner_name + REPO = repo_name + GITHUB_API_URL = '%s/repos/%s/%s' % (BASE_URL, OWNER, REPO) + return True + + +def requests_retry_session(retries=RETRIES, + backoff_factor=BACKOFF, + status_forcelist=RETRY_STATUS): + session = requests.Session() + retry = Retry(total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + +def get_pull_request_review_comments(token, pull_number, since=None): + """https://docs.github.com/en/rest/pulls/comments#list-review-comments-on-a-pull-request""" + url = f'{GITHUB_API_URL}/pulls/{pull_number}/comments' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + + page = 1 + per_page = 100 # GitHub API default and max is 100 for many paginated endpoints + results = [] + + base_params = {'per_page': per_page} + if since: + base_params['since'] = since + + while True: + current_page_params = base_params.copy() + current_page_params['page'] = page + + try: + with requests_retry_session().get(url, headers=headers, params=current_page_params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + + current_page_results = response.json() + if not current_page_results: # No more data + break + + results.extend(current_page_results) + + if len(current_page_results) < per_page: # Reached last page + break + + page += 1 + + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to fetch review comments (page {page}, params: {current_page_params}) for PR {pull_number}: {e}\n") + return None + return results + + +def list_pull_requests(token, state, head, base): + """https://docs.github.com/en/rest/reference/pulls#list-pull-requests""" + url = f'{GITHUB_API_URL}/pulls' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + page = 1 + per_page = 100 + results = [] + keep_going = True + while keep_going: + params = {'per_page': per_page, 'page': page} + if state: params.update({'state': state}) + if head: params.update({'head': head}) + if base: params.update({'base': base}) + page = page + 1 + keep_going = False + try: + with requests_retry_session().get(url, headers=headers, params=params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + current_page_results = response.json() + if not current_page_results: + break + results.extend(current_page_results) + keep_going = (len(current_page_results) == per_page) + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to list pull requests (page {params.get('page', 'N/A')}, params: {params}) for {OWNER}/{REPO}: {e}\n") + return None + return results + + +def get_pull_request_reviews(token, owner, repo, pull_number): + """Fetches all reviews for a given pull request.""" + # Note: GitHub API for listing reviews does not support a 'since' parameter directly. + # Filtering by 'since' must be done client-side after fetching all reviews. + url = f'{GITHUB_API_URL}/pulls/{pull_number}/reviews' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + page = 1 + per_page = 100 + results = [] + keep_going = True + while keep_going: + params = {'per_page': per_page, 'page': page} + page = page + 1 + keep_going = False + try: + with requests_retry_session().get(url, headers=headers, params=params, + stream=True, timeout=TIMEOUT) as response: + response.raise_for_status() + current_page_results = response.json() + if not current_page_results: + break + results.extend(current_page_results) + keep_going = (len(current_page_results) == per_page) + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to list pull request reviews (page {params.get('page', 'N/A')}, params: {params}) for PR {pull_number} in {owner}/{repo}: {e}\n") + return None + return results + + +def get_current_branch_name(): + """Gets the current git branch name.""" + try: + branch_bytes = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.PIPE) + return branch_bytes.decode().strip() + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Could not determine current git branch: {e}\n") + return None + +def get_latest_pr_for_branch(token, owner, repo, branch_name): + """Fetches the most recent open pull request for a given branch.""" + if not owner or not repo: + sys.stderr.write("Owner and repo must be set to find PR for branch.\n") + return None + + head_branch_spec = f"{owner}:{branch_name}" # Format required by GitHub API for head branch + prs = list_pull_requests(token=token, state="open", head=head_branch_spec, base=None) + + if not prs: + return None + + # Sort PRs by creation date (most recent first) to find the latest. + try: + prs.sort(key=lambda pr: pr.get("created_at", ""), reverse=True) + except Exception as e: # Broad exception for safety, though sort issues are rare with valid data. + sys.stderr.write(f"Could not sort PRs by creation date: {e}\n") + return None + + if prs: + return prs[0].get("number") + return None + + +def main(): + STATUS_IRRELEVANT = "[IRRELEVANT]" + STATUS_OLD = "[OLD]" + STATUS_CURRENT = "[CURRENT]" + + determined_owner = None + determined_repo = None + try: + git_url_bytes = subprocess.check_output(["git", "remote", "get-url", "origin"], stderr=subprocess.PIPE) + git_url = git_url_bytes.decode().strip() + match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+)(?:\.git)?", git_url) + if match: + determined_owner = match.group(1) + determined_repo = match.group(2) + sys.stderr.write(f"Determined repository: {determined_owner}/{determined_repo} from git remote.\n") + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Could not automatically determine repository from git remote: {e}\n") + except Exception as e: # Catch any other unexpected error. + sys.stderr.write(f"An unexpected error occurred while determining repository: {e}\n") + + def parse_repo_url(url_string): + """Parses owner and repository name from various GitHub URL formats.""" + url_match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+?)(?:\.git)?/?$", url_string) + if url_match: + return url_match.group(1), url_match.group(2) + return None, None + + parser = argparse.ArgumentParser( + description="Fetch review comments from a GitHub PR and format into simple text output.\n" + "Repository can be specified via --url, or --owner AND --repo, or auto-detected from git remote 'origin'.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--pull_number", + type=int, + default=None, + help="Pull request number. If not provided, script attempts to find the latest open PR for the current git branch." + ) + parser.add_argument( + "--branch", + type=str, + default=None, + help="Branch name to find the latest open PR for. Mutually exclusive with --pull_number. If neither --pull_number nor --branch is provided, uses the current git branch." + ) + parser.add_argument( + "--url", + type=str, + default=None, + help="Full GitHub repository URL (e.g., https://github.com/owner/repo or git@github.com:owner/repo.git). Takes precedence over --owner/--repo." + ) + parser.add_argument( + "--owner", + type=str, + default=determined_owner, + help=f"Repository owner. Used if --url is not provided. {'Default: ' + determined_owner if determined_owner else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--repo", + type=str, + default=determined_repo, + help=f"Repository name. Used if --url is not provided. {'Default: ' + determined_repo if determined_repo else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--token", + type=str, + default=os.environ.get("GITHUB_TOKEN"), + help="GitHub token. Can also be set via GITHUB_TOKEN env var or from ~/.github_token." + ) + parser.add_argument( + "--context-lines", + type=int, + default=10, + help="Number of context lines from the diff hunk. 0 for full hunk. If > 0, shows header (if any) and last N lines of the remaining hunk. Default: 10." + ) + parser.add_argument( + "--since", + type=str, + default=None, + help="Only show comments updated at or after this ISO 8601 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ)." + ) + parser.add_argument( + "--exclude-old", + action="store_true", + default=False, + help="Exclude comments marked [OLD] (where line number has changed due to code updates but position is still valid)." + ) + parser.add_argument( + "--include-irrelevant", + action="store_true", + default=False, + help="Include comments marked [IRRELEVANT] (where GitHub can no longer anchor the comment to the diff, i.e., position is null)." + ) + + args = parser.parse_args() + error_suffix = " (use --help for more details)" + + # Initialize tracking variables early, including processed_comments_count + latest_overall_review_activity_dt = None + latest_line_comment_activity_dt = None + processed_comments_count = 0 + + token = args.token + if not token: + try: + with open(os.path.expanduser("~/.github_token"), "r") as f: + token = f.read().strip() + if token: + sys.stderr.write("Using token from ~/.github_token\n") + except FileNotFoundError: + pass # File not found is fine, we'll check token next + except Exception as e: + sys.stderr.write(f"Warning: Could not read ~/.github_token: {e}\n") + + + if not token: + sys.stderr.write(f"Error: GitHub token not provided. Set GITHUB_TOKEN, use --token, or place it in ~/.github_token.{error_suffix}\n") + sys.exit(1) + args.token = token # Ensure args.token is populated for the rest of the script + + final_owner = None + final_repo = None + + # Determine repository owner and name + if args.url: + owner_explicitly_set = args.owner is not None and args.owner != determined_owner + repo_explicitly_set = args.repo is not None and args.repo != determined_repo + if owner_explicitly_set or repo_explicitly_set: + sys.stderr.write(f"Error: Cannot use --owner or --repo when --url is specified.{error_suffix}\n") + sys.exit(1) + + parsed_owner, parsed_repo = parse_repo_url(args.url) + if parsed_owner and parsed_repo: + final_owner = parsed_owner + final_repo = parsed_repo + sys.stderr.write(f"Using repository from --url: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Invalid URL format: {args.url}. Expected https://github.com/owner/repo or git@github.com:owner/repo.git{error_suffix}\n") + sys.exit(1) + else: + is_owner_from_user = args.owner is not None and args.owner != determined_owner + is_repo_from_user = args.repo is not None and args.repo != determined_repo + + if (is_owner_from_user or is_repo_from_user): # User explicitly set at least one of owner/repo + if args.owner and args.repo: + final_owner = args.owner + final_repo = args.repo + sys.stderr.write(f"Using repository from --owner/--repo args: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Both --owner and --repo must be specified if one is provided explicitly (and --url is not used).{error_suffix}\n") + sys.exit(1) + elif args.owner and args.repo: # Both args have values, from successful auto-detection + final_owner = args.owner + final_repo = args.repo + elif args.owner or args.repo: # Only one has a value from auto-detection (e.g. git remote parsing failed partially) + sys.stderr.write(f"Error: Both --owner and --repo are required if not using --url, and auto-detection was incomplete.{error_suffix}\n") + sys.exit(1) + # If final_owner/repo are still None here, it means auto-detection failed AND user provided nothing. + + if not final_owner or not final_repo: + sys.stderr.write(f"Error: Could not determine repository. Please specify --url, OR both --owner and --repo, OR ensure git remote 'origin' is configured correctly.{error_suffix}\n") + sys.exit(1) + + if not set_repo_url_standalone(final_owner, final_repo): + sys.stderr.write(f"Error: Could not set repository to {final_owner}/{final_repo}. Ensure owner/repo are correct.{error_suffix}\n") + sys.exit(1) + + pull_request_number = args.pull_number + branch_to_find_pr_for = None + + if args.pull_number and args.branch: + sys.stderr.write(f"Error: --pull_number and --branch are mutually exclusive.{error_suffix}\n") + sys.exit(1) + + if not pull_request_number: + if args.branch: + branch_to_find_pr_for = args.branch + sys.stderr.write(f"Pull number not specified, attempting to find PR for branch: {branch_to_find_pr_for}...\n") + else: + sys.stderr.write("Pull number and branch not specified, attempting to find PR for current git branch...\n") + branch_to_find_pr_for = get_current_branch_name() + if branch_to_find_pr_for: + sys.stderr.write(f"Current git branch is: {branch_to_find_pr_for}\n") + else: + sys.stderr.write(f"Error: Could not determine current git branch. Cannot find PR automatically.{error_suffix}\n") + sys.exit(1) + + if branch_to_find_pr_for: # This will be true if args.branch was given, or if get_current_branch_name() succeeded + pull_request_number = get_latest_pr_for_branch(args.token, OWNER, REPO, branch_to_find_pr_for) + if pull_request_number: + sys.stderr.write(f"Found PR #{pull_request_number} for branch {branch_to_find_pr_for}.\n") + else: + sys.stderr.write(f"No open PR found for branch {branch_to_find_pr_for} in {OWNER}/{REPO}.\n") + # If branch_to_find_pr_for is None here, it means get_current_branch_name() failed and we already exited. + + if not pull_request_number: # Final check for PR number + error_message = "Error: Pull request number could not be determined." + if args.branch: # Specific error if --branch was used + error_message = f"Error: No open PR found for specified branch '{args.branch}'." + elif not args.pull_number and branch_to_find_pr_for: # Auto-detect current branch ok, but no PR found + error_message = f"Error: Pull request number not specified and no open PR found for current branch '{branch_to_find_pr_for}'." + # The case where current_branch_for_pr_check (now branch_to_find_pr_for) is None (git branch fail) is caught and exited above. + sys.stderr.write(f"{error_message}{error_suffix}\n") + sys.exit(1) + + sys.stderr.write(f"Fetching overall reviews for PR #{pull_request_number} from {OWNER}/{REPO}...\n") + overall_reviews = get_pull_request_reviews(args.token, OWNER, REPO, pull_request_number) + + if overall_reviews is None: + sys.stderr.write(f"Error: Failed to fetch overall reviews due to an API or network issue.{error_suffix}\nPlease check logs for details.\n") + sys.exit(1) + + filtered_overall_reviews = [] + if overall_reviews: # If not None and not empty + for review in overall_reviews: + review_state = review.get("state") + if review_state == "DISMISSED" or review_state == "PENDING": + continue + + if args.since: + submitted_at_str = review.get("submitted_at") + if submitted_at_str: + try: + # Compatibility for Python < 3.11 + if sys.version_info < (3, 11): + dt_str_submitted = submitted_at_str.replace("Z", "+00:00") + else: + dt_str_submitted = submitted_at_str + submitted_dt = datetime.datetime.fromisoformat(dt_str_submitted) + + since_dt_str = args.since + if sys.version_info < (3, 11) and args.since.endswith("Z"): + since_dt_str = args.since.replace("Z", "+00:00") + since_dt = datetime.datetime.fromisoformat(since_dt_str) + + # Ensure 'since_dt' is timezone-aware if 'submitted_dt' is. + # GitHub timestamps are UTC. fromisoformat on Z or +00:00 makes them aware. + if submitted_dt.tzinfo and not since_dt.tzinfo: + since_dt = since_dt.replace(tzinfo=timezone.utc) # Assume since is UTC if not specified + + if submitted_dt < since_dt: + continue + except ValueError as ve: + sys.stderr.write(f"Warning: Could not parse review submitted_at timestamp '{submitted_at_str}' or --since timestamp '{args.since}': {ve}\n") + # If parsing fails, we might choose to include the review to be safe, or skip. Current: include. + + if review.get("state") == "COMMENTED" and not review.get("body", "").strip(): + continue + + filtered_overall_reviews.append(review) + + try: + filtered_overall_reviews.sort(key=lambda r: r.get("submitted_at", "")) + except Exception as e: # Broad exception for safety + sys.stderr.write(f"Warning: Could not sort overall reviews: {e}\n") + + if filtered_overall_reviews: + print("# Code Reviews\n\n") + # Use a temporary variable for accumulating latest timestamp within this specific block + temp_latest_overall_review_dt = None + for review in filtered_overall_reviews: + user = review.get("user", {}).get("login", "Unknown user") + submitted_at_str = review.get("submitted_at", "N/A") + state = review.get("state", "N/A") + body = review.get("body", "").strip() + + if submitted_at_str and submitted_at_str != "N/A": + try: + if sys.version_info < (3, 11): + dt_str_submitted = submitted_at_str.replace("Z", "+00:00") + else: + dt_str_submitted = submitted_at_str + current_review_submitted_dt = datetime.datetime.fromisoformat(dt_str_submitted) + if temp_latest_overall_review_dt is None or current_review_submitted_dt > temp_latest_overall_review_dt: + temp_latest_overall_review_dt = current_review_submitted_dt + except ValueError: + sys.stderr.write(f"Warning: Could not parse overall review submitted_at for --since suggestion: {submitted_at_str}\n") + + html_url = review.get("html_url", "N/A") + review_id = review.get("id", "N/A") + + print(f"## Review by: **{user}** (ID: `{review_id}`)\n") + print(f"* **Submitted At**: `{submitted_at_str}`") + print(f"* **State**: `{state}`") + print(f"* **URL**: <{html_url}>\n") + + if body: + print("\n### Comment:") # Changed heading + print(body) + print("\n---") + + # After processing all overall reviews in this block, update the main variable + if temp_latest_overall_review_dt: + latest_overall_review_activity_dt = temp_latest_overall_review_dt + print("\n") + + + sys.stderr.write(f"Fetching line comments for PR #{pull_request_number} from {OWNER}/{REPO}...\n") + if args.since: + sys.stderr.write(f"Filtering line comments updated since: {args.since}\n") + + comments = get_pull_request_review_comments( + args.token, + pull_request_number, + since=args.since + ) + + if comments is None: + sys.stderr.write(f"Error: Failed to fetch line comments due to an API or network issue.{error_suffix}\nPlease check logs for details.\n") + sys.exit(1) + # Note: The decision to exit if only line comments fail vs. if only overall reviews fail could be nuanced. + # For now, failure to fetch either is treated as a critical error for the script's purpose. + + # Handling for line comments + if not comments: + sys.stderr.write(f"No line comments found for PR #{pull_request_number} (or matching filters).\n") + # If filtered_overall_reviews is also empty, then overall_latest_activity_dt will be None, + # and no 'next command' suggestion will be printed. This is correct. + else: + print("# Review Comments\n\n") + + for comment in comments: + created_at_str = comment.get("created_at") + + current_pos = comment.get("position") + current_line = comment.get("line") + original_line = comment.get("original_line") + + status_text = "" + line_to_display = None + + if current_pos is None: + status_text = STATUS_IRRELEVANT + line_to_display = original_line + elif original_line is not None and current_line != original_line: + status_text = STATUS_OLD + line_to_display = current_line + else: + status_text = STATUS_CURRENT + line_to_display = current_line + + if line_to_display is None: + line_to_display = "N/A" + + if status_text == STATUS_IRRELEVANT and not args.include_irrelevant: + continue + if status_text == STATUS_OLD and args.exclude_old: + continue + + updated_at_str = comment.get("updated_at") + if updated_at_str: + try: + # Compatibility for Python < 3.11 which doesn't handle 'Z' suffix in fromisoformat + if sys.version_info < (3, 11): + dt_str_updated = updated_at_str.replace("Z", "+00:00") + else: + dt_str_updated = updated_at_str + current_comment_activity_dt = datetime.datetime.fromisoformat(dt_str_updated) + if latest_line_comment_activity_dt is None or current_comment_activity_dt > latest_line_comment_activity_dt: + latest_line_comment_activity_dt = current_comment_activity_dt + except ValueError: + sys.stderr.write(f"Warning: Could not parse line comment updated_at for --since suggestion: {updated_at_str}\n") + + user = comment.get("user", {}).get("login", "Unknown user") + path = comment.get("path", "N/A") + body = comment.get("body", "").strip() + + if not body: + continue + + processed_comments_count += 1 + + diff_hunk = comment.get("diff_hunk") + html_url = comment.get("html_url", "N/A") + comment_id = comment.get("id") + in_reply_to_id = comment.get("in_reply_to_id") + + print(f"## Comment by: **{user}** (ID: `{comment_id}`){f' (In Reply To: `{in_reply_to_id}`)' if in_reply_to_id else ''}\n") + if created_at_str: + print(f"* **Timestamp**: `{created_at_str}`") + print(f"* **Status**: `{status_text}`") + print(f"* **File**: `{path}`") + print(f"* **Line**: `{line_to_display}`") + print(f"* **URL**: <{html_url}>\n") + + print("\n### Context:") + print("```") + if diff_hunk and diff_hunk.strip(): + if args.context_lines == 0: + print(diff_hunk) + else: + hunk_lines = diff_hunk.split('\n') + if hunk_lines and hunk_lines[0].startswith("@@ "): + print(hunk_lines[0]) + hunk_lines = hunk_lines[1:] + print("\n".join(hunk_lines[-args.context_lines:])) + else: + print("(No diff hunk available for this comment)") + print("```") + + print("\n### Comment:") + print(body) + print("\n---") + + sys.stderr.write(f"\nPrinted {processed_comments_count} comments to stdout.\n") + + # Determine the overall latest activity timestamp + overall_latest_activity_dt = None + if latest_overall_review_activity_dt and latest_line_comment_activity_dt: + overall_latest_activity_dt = max(latest_overall_review_activity_dt, latest_line_comment_activity_dt) + elif latest_overall_review_activity_dt: + overall_latest_activity_dt = latest_overall_review_activity_dt + elif latest_line_comment_activity_dt: + overall_latest_activity_dt = latest_line_comment_activity_dt + + if overall_latest_activity_dt: + try: + next_since_dt = overall_latest_activity_dt.astimezone(timezone.utc) + timedelta(seconds=2) + next_since_str = next_since_dt.strftime('%Y-%m-%dT%H:%M:%SZ') + + new_cmd_args = [sys.executable, sys.argv[0]] + i = 1 + while i < len(sys.argv): + if sys.argv[i] == "--since": + i += 2 + continue + new_cmd_args.append(sys.argv[i]) + i += 1 + + new_cmd_args.extend(["--since", next_since_str]) + suggested_cmd = " ".join(new_cmd_args) + sys.stderr.write(f"\nTo get comments created after the last one in this batch, try:\n{suggested_cmd}\n") + except Exception as e: + sys.stderr.write(f"\nWarning: Could not generate next command suggestion: {e}\n") + +if __name__ == "__main__": + main() diff --git a/scripts/update_android_ios_dependencies.py b/scripts/update_android_ios_dependencies.py index f29e3ec07f..c8517f5e4f 100644 --- a/scripts/update_android_ios_dependencies.py +++ b/scripts/update_android_ios_dependencies.py @@ -185,18 +185,6 @@ def get_files(dirs_and_files, file_extension, file_name=None, 'FirebaseStorage', ] -# List of GMA pods we are also interested in. -PODS_GMA = [ - 'Google-Mobile-Ads-SDK', - 'GoogleUserMessagingPlatform' -] - -ANDROID_GMA_PACKAGES = [ - 'firebase-ads', - 'play-services-ads', - 'user-messaging-platform', -] - def get_pod_versions(specs_repo, pods=PODS, ignore_pods=None, allow_experimental=False): """Get available pods and their versions from the specs repo @@ -727,8 +715,6 @@ def parse_cmdline_args(): parser.add_argument('--ignore_android_packages', nargs='+', default=(), help='Ignore Android packages which have any of the items ' 'specified in this list as substrings.') - parser.add_argument('--include_gma', action='store_true', - help='Also update GMA dependencies') parser.add_argument('--depfiles', nargs='+', default=('Android/firebase_dependencies.gradle', 'release_build_files/Android/firebase_dependencies.gradle'), @@ -775,7 +761,7 @@ def main(): if not args.skip_ios: latest_pod_versions_map = get_latest_pod_versions( args.specs_repo, - (PODS + PODS_GMA) if args.include_gma else PODS, + PODS, set(args.ignore_ios_pods), args.allow_experimental) pod_files = get_files(args.podfiles, file_extension='', file_name='Podfile', ignore_directories=set(args.ignore_directories)) @@ -786,11 +772,8 @@ def main(): modify_readme_file_pods(readme_file, latest_pod_versions_map, args.dryrun) if not args.skip_android: - ignore_android_packages = set(args.ignore_android_packages) - if not args.include_gma: - ignore_android_packages.update(ANDROID_GMA_PACKAGES) latest_android_versions_map = get_latest_maven_versions( - ignore_android_packages, args.allow_experimental) + set(args.ignore_android_packages), args.allow_experimental) dep_files = get_files(args.depfiles, file_extension='.gradle', file_name='firebase_dependencies.gradle', ignore_directories=set(args.ignore_directories)) diff --git a/settings.gradle b/settings.gradle index 0ec81050fc..759afe58eb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,12 +14,12 @@ include ':app', ':firestore', ':firestore:firestore_resources', ':functions', - ':gma', - ':gma:gma_resources', ':installations', ':messaging', ':messaging:messaging_java', ':remote_config', ':remote_config:remote_config_resources', ':storage', - ':storage:storage_resources' \ No newline at end of file + ':storage:storage_resources', + ':ump', + ':ump:ump_resources' diff --git a/setup_integration_tests.py b/setup_integration_tests.py index 7c37e12fc8..93fa787aa0 100755 --- a/setup_integration_tests.py +++ b/setup_integration_tests.py @@ -40,11 +40,11 @@ 'firestore/integration_test', 'firestore/integration_test_internal', 'functions/integration_test', - 'gma/integration_test', 'installations/integration_test', 'messaging/integration_test', 'remote_config/integration_test', 'storage/integration_test', + 'ump/integration_test', ] destinations = sys.argv[1:] if len(sys.argv) > 1 else DEFAULT_DESTINATIONS diff --git a/storage/build.gradle b/storage/build.gradle index 00444f11d1..977e770ac3 100644 --- a/storage/build.gradle +++ b/storage/build.gradle @@ -88,4 +88,7 @@ project.afterEvaluate { setupDexDependencies(':storage:storage_resources') preBuild.dependsOn(':app:build') preBuild.dependsOn(':auth:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } diff --git a/storage/integration_test/Podfile b/storage/integration_test/Podfile index 0e2f2e4fd6..db4c743a63 100644 --- a/storage/integration_test/Podfile +++ b/storage/integration_test/Podfile @@ -4,14 +4,14 @@ use_frameworks! :linkage => :static target 'integration_test' do platform :ios, '13.0' - pod 'Firebase/Storage', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Storage', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end target 'integration_test_tvos' do platform :tvos, '13.0' - pod 'Firebase/Storage', '11.10.0' - pod 'Firebase/Auth', '11.10.0' + pod 'Firebase/Storage', '11.14.0' + pod 'Firebase/Auth', '11.14.0' end post_install do |installer| diff --git a/storage/integration_test/build.gradle b/storage/integration_test/build.gradle index ed536d709f..34c3f22f4c 100644 --- a/storage/integration_test/build.gradle +++ b/storage/integration_test/build.gradle @@ -101,3 +101,9 @@ task copyIntegrationTestFiles(type:Exec) { } build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/storage/storage_resources/build.gradle b/storage/storage_resources/build.gradle index 24a407fe26..f1bfb0f1d4 100644 --- a/storage/storage_resources/build.gradle +++ b/storage/storage_resources/build.gradle @@ -54,13 +54,16 @@ android { } dependencies { - implementation platform('com.google.firebase:firebase-bom:33.11.0') + implementation platform('com.google.firebase:firebase-bom:33.15.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-storage' } afterEvaluate { generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" diff --git a/ump/CMakeLists.txt b/ump/CMakeLists.txt new file mode 100644 index 0000000000..2739decefa --- /dev/null +++ b/ump/CMakeLists.txt @@ -0,0 +1,114 @@ +# 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. + +# CMake file for the firebase_ump library + +# Common source files used by all platforms +set(common_SRCS + src/common/consent_info.cc + src/common/consent_info_internal.cc +) + +# Define the resource build needed for Android +firebase_cpp_gradle(":ump:ump_resources:generateDexJarRelease" + "${CMAKE_CURRENT_LIST_DIR}/ump_resources/build/ump_resources_lib.jar") +binary_to_array("ump_resources" + "${CMAKE_CURRENT_LIST_DIR}/ump_resources/build/ump_resources_lib.jar" + "firebase_ump" + "${FIREBASE_GEN_FILE_DIR}/ump") + +# Source files used by the Android implementation. +set(android_SRCS + ${ump_resources_source} + src/android/consent_info_internal_android.cc +) + +# Source files used by the iOS implementation. +set(ios_SRCS + src/ios/consent_info_internal_ios.mm +) + +# Source files used by the stub implementation. +set(stub_SRCS + src/stub/consent_info_internal_stub.cc +) + +if(ANDROID) + set(ump_platform_SRCS + "${android_SRCS}") +elseif(IOS) + set(ump_platform_SRCS + "${ios_SRCS}") +else() + set(ump_platform_SRCS + "${stub_SRCS}") +endif() + +add_library(firebase_ump STATIC + ${common_SRCS} + ${ump_platform_SRCS}) + +set_property(TARGET firebase_ump PROPERTY FOLDER "Firebase Cpp") + +# Set up the dependency on Firebase App. +target_link_libraries(firebase_ump + PUBLIC firebase_app) +# Public headers all refer to each other relative to the src/include directory, +# while private headers are relative to the entire C++ SDK directory. +target_include_directories(firebase_ump + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/src/include + PRIVATE + ${FIREBASE_CPP_SDK_ROOT_DIR} +) +target_compile_definitions(firebase_ump + PRIVATE + -DINTERNAL_EXPERIMENTAL=1 +) +# Automatically include headers that might not be declared. +if(MSVC) + add_definitions(/FI"assert.h" /FI"string.h" /FI"stdint.h") +else() + add_definitions(-include assert.h -include string.h) +endif() + +if(ANDROID) + firebase_cpp_proguard_file(ump) +elseif(IOS) + # UMP for iOS uses weak references, which requires enabling Automatic + # Reference Counting (ARC). Also enable BitCode. + target_compile_options(firebase_ump + PUBLIC "-fobjc-arc" "-fembed-bitcode") + target_link_libraries(firebase_ump + PUBLIC "-fembed-bitcode") + + setup_pod_headers( + firebase_ump + POD_NAMES + GoogleUserMessagingPlatform + ) + + # UMP expects the header files to be in a subfolder, so set up a symlink to + # accomplish that. + symlink_pod_headers(firebase_ump GoogleUserMessagingPlatform UserMessagingPlatform) + + if (FIREBASE_XCODE_TARGET_FORMAT STREQUAL "frameworks") + set_target_properties(firebase_ump PROPERTIES + FRAMEWORK TRUE + ) + endif() +endif() + +cpp_pack_library(firebase_ump "") +cpp_pack_public_headers() diff --git a/ump/build.gradle b/ump/build.gradle new file mode 100644 index 0000000000..4eff85a1cf --- /dev/null +++ b/ump/build.gradle @@ -0,0 +1,93 @@ +// 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. + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + } +} +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 34 + ndkPath System.getenv('ANDROID_NDK_HOME') + buildToolsVersion '32.0.0' + + sourceSets { + main { + manifest.srcFile '../android_build_files/AndroidManifest.xml' + } + } + + externalNativeBuild { + cmake { + path '../CMakeLists.txt' + } + } + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 34 + versionCode 1 + versionName "1.0" + + buildTypes { + release { + minifyEnabled false + } + } + + externalNativeBuild { + cmake { + targets 'firebase_ump' + // Args are: Re-use app library prebuilt by app gradle project. + // Don't configure all the cmake subprojects. + // Only include needed project. + arguments '-DFIREBASE_CPP_USE_PRIOR_GRADLE_BUILD=ON', + '-DFIREBASE_INCLUDE_LIBRARY_DEFAULT=OFF', + '-DFIREBASE_INCLUDE_UMP=ON' + } + } + } + + lintOptions { + abortOnError false + } +} + +dependencies { + implementation project(':app') +} +apply from: "$rootDir/android_build_files/android_abis.gradle" +apply from: "$rootDir/android_build_files/extract_and_dex.gradle" +apply from: "$rootDir/android_build_files/generate_proguard.gradle" +project.afterEvaluate { + generateProguardFile('ump') + setupDexDependencies(':ump:ump_resources') + preBuild.dependsOn(':app:build') + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/ump/integration_test/AndroidManifest.xml b/ump/integration_test/AndroidManifest.xml new file mode 100644 index 0000000000..fa4da43dd1 --- /dev/null +++ b/ump/integration_test/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ump/integration_test/CMakeLists.txt b/ump/integration_test/CMakeLists.txt new file mode 100644 index 0000000000..5db68bc397 --- /dev/null +++ b/ump/integration_test/CMakeLists.txt @@ -0,0 +1,242 @@ +# 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. + +# Cmake file for a single C++ integration test build. + +cmake_minimum_required(VERSION 2.8) + +find_program(FIREBASE_PYTHON_EXECUTABLE + NAMES python3 python + DOC "The Python interpreter to use, such as one from a venv" + REQUIRED +) + +# User settings for Firebase integration tests. +# Path to Firebase SDK. +# Try to read the path to the Firebase C++ SDK from an environment variable. +if (NOT "$ENV{FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(DEFAULT_FIREBASE_CPP_SDK_DIR "$ENV{FIREBASE_CPP_SDK_DIR}") +else() + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../../cpp_sdk_version.json") + set(DEFAULT_FIREBASE_CPP_SDK_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") + else() + set(DEFAULT_FIREBASE_CPP_SDK_DIR "firebase_cpp_sdk") + endif() +endif() +if ("${FIREBASE_CPP_SDK_DIR}" STREQUAL "") + set(FIREBASE_CPP_SDK_DIR ${DEFAULT_FIREBASE_CPP_SDK_DIR}) +endif() +if(NOT EXISTS ${FIREBASE_CPP_SDK_DIR}) + message(FATAL_ERROR "The Firebase C++ SDK directory does not exist: ${FIREBASE_CPP_SDK_DIR}. See the readme.md for more information") +endif() + +# Copy all prerequisite files for integration tests to run. +if(NOT ANDROID) + if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/../../setup_integration_tests.py) + # If this is running from inside the SDK directory, run the setup script. + execute_process( + COMMAND + ${FIREBASE_PYTHON_EXECUTABLE} + "${CMAKE_CURRENT_LIST_DIR}/../../setup_integration_tests.py" + "${CMAKE_CURRENT_LIST_DIR}" + RESULT_VARIABLE + FIREBASE_PYTHON_EXECUTABLE_RESULT + ) + if(NOT FIREBASE_PYTHON_EXECUTABLE_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to run setup_integration_tests.py") + endif() + endif() +endif() + +# Windows runtime mode, either MD or MT depending on whether you are using +# /MD or /MT. For more information see: +# https://msdn.microsoft.com/en-us/library/2kzt1wy3.aspx +set(MSVC_RUNTIME_MODE MD) + +project(firebase_testapp) + +# Integration test source files. +set(FIREBASE_APP_FRAMEWORK_SRCS + src/app_framework.cc + src/app_framework.h +) + +set(FIREBASE_TEST_FRAMEWORK_SRCS + src/firebase_test_framework.h + src/firebase_test_framework.cc +) + +set(FIREBASE_INTEGRATION_TEST_SRCS + src/integration_test.cc +) + +# The include directory for the testapp. +include_directories(src) + +# Firebase C++ SDK requires C++14. +set (CMAKE_CXX_STANDARD 14) +set (CMAKE_CXX_STANDARD_REQUIRED YES) # Don't fall back to an earlier version. + +# Download and unpack googletest (and googlemock) at configure time +set(GOOGLETEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/external/googletest) +# Note: Once googletest is downloaded once, it won't be updated or +# downloaded again unless you delete the "external/googletest" +# directory. +if (NOT EXISTS ${GOOGLETEST_ROOT}/src/googletest/src/gtest-all.cc) + configure_file(googletest.cmake + ${CMAKE_CURRENT_LIST_DIR}/external/googletest/CMakeLists.txt COPYONLY) + execute_process(COMMAND ${CMAKE_COMMAND} . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/external/googletest ) + if(result) + message(FATAL_ERROR "CMake step for googletest failed: ${result}") + endif() + execute_process(COMMAND ${CMAKE_COMMAND} --build . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/external/googletest ) + if(result) + message(FATAL_ERROR "Build step for googletest failed: ${result}") + endif() +endif() + +if(ANDROID) + # Build an Android application. + + # Source files used for the Android build. + set(FIREBASE_APP_FRAMEWORK_ANDROID_SRCS + src/android/android_app_framework.cc + ) + + # Source files used for the Android build. + set(FIREBASE_TEST_FRAMEWORK_ANDROID_SRCS + src/android/android_firebase_test_framework.cc + ) + + # Build native_app_glue as a static lib + add_library(native_app_glue STATIC + ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c) + + # Export ANativeActivity_onCreate(), + # Refer to: https://github.com/android-ndk/ndk/issues/381. + set(CMAKE_SHARED_LINKER_FLAGS + "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate") + + add_library(gtest STATIC + ${GOOGLETEST_ROOT}/src/googletest/src/gtest-all.cc) + target_include_directories(gtest + PRIVATE ${GOOGLETEST_ROOT}/src/googletest + PUBLIC ${GOOGLETEST_ROOT}/src/googletest/include) + add_library(gmock STATIC + ${GOOGLETEST_ROOT}/src/googlemock/src/gmock-all.cc) + target_include_directories(gmock + PRIVATE ${GOOGLETEST_ROOT}/src/googletest + PRIVATE ${GOOGLETEST_ROOT}/src/googlemock + PUBLIC ${GOOGLETEST_ROOT}/src/googletest/include + PUBLIC ${GOOGLETEST_ROOT}/src/googlemock/include) + + # Define the target as a shared library, as that is what gradle expects. + set(integration_test_target_name "android_integration_test_main") + add_library(${integration_test_target_name} SHARED + ${FIREBASE_APP_FRAMEWORK_SRCS} + ${FIREBASE_APP_FRAMEWORK_ANDROID_SRCS} + ${FIREBASE_INTEGRATION_TEST_SRCS} + ${FIREBASE_TEST_FRAMEWORK_SRCS} + ${FIREBASE_TEST_FRAMEWORK_ANDROID_SRCS} + ) + + target_include_directories(${integration_test_target_name} PRIVATE + ${ANDROID_NDK}/sources/android/native_app_glue) + + set(ADDITIONAL_LIBS log android atomic native_app_glue) +else() + # Build a desktop application. + add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0) + + # Prevent overriding the parent project's compiler/linker + # settings on Windows + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + + # Add googletest directly to our build. This defines + # the gtest and gtest_main targets. + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/external/googletest/src + ${CMAKE_CURRENT_LIST_DIR}/external/googletest/build + EXCLUDE_FROM_ALL) + + # The gtest/gtest_main targets carry header search path + # dependencies automatically when using CMake 2.8.11 or + # later. Otherwise we have to add them here ourselves. + if (CMAKE_VERSION VERSION_LESS 2.8.11) + include_directories("${gtest_SOURCE_DIR}/include") + include_directories("${gmock_SOURCE_DIR}/include") + endif() + + # Windows runtime mode, either MD or MT depending on whether you are using + # /MD or /MT. For more information see: + # https://msdn.microsoft.com/en-us/library/2kzt1wy3.aspx + set(MSVC_RUNTIME_MODE MD) + + # Platform abstraction layer for the desktop integration test. + set(FIREBASE_APP_FRAMEWORK_DESKTOP_SRCS + src/desktop/desktop_app_framework.cc + src/desktop/desktop_firebase_test_framework.cc + ) + + set(integration_test_target_name "integration_test") + add_executable(${integration_test_target_name} + ${FIREBASE_APP_FRAMEWORK_SRCS} + ${FIREBASE_APP_FRAMEWORK_DESKTOP_SRCS} + ${FIREBASE_TEST_FRAMEWORK_SRCS} + ${FIREBASE_INTEGRATION_TEST_SRCS} + ) + + if(APPLE) + set(ADDITIONAL_LIBS + gssapi_krb5 + pthread + "-framework CoreFoundation" + "-framework Foundation" + "-framework GSS" + "-framework Security" + ) + elseif(MSVC) + set(ADDITIONAL_LIBS advapi32 ws2_32 crypt32) + else() + set(ADDITIONAL_LIBS pthread) + endif() + + # If a config file is present, copy it into the binary location so that it's + # possible to create the default Firebase app. + set(FOUND_JSON_FILE FALSE) + foreach(config "google-services-desktop.json" "google-services.json") + if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/${config}") + add_custom_command( + TARGET ${integration_test_target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_LIST_DIR}/${config}" $) + set(FOUND_JSON_FILE TRUE) + break() + endif() + endforeach() + if(NOT FOUND_JSON_FILE) + message(WARNING "Failed to find either google-services-desktop.json or google-services.json. See the readme.md for more information.") + endif() +endif() + +# Add the Firebase libraries to the target using the function from the SDK. +add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL) +# Note that firebase_app needs to be last in the list. +set(firebase_libs firebase_ump firebase_app) +set(gtest_libs gtest gmock) +target_link_libraries(${integration_test_target_name} ${firebase_libs} + ${gtest_libs} ${ADDITIONAL_LIBS}) diff --git a/ump/integration_test/Images.xcassets/AppIcon.appiconset/Contents.json b/ump/integration_test/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d8db8d65fd --- /dev/null +++ b/ump/integration_test/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ump/integration_test/Images.xcassets/LaunchImage.launchimage/Contents.json b/ump/integration_test/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..6f870a4629 --- /dev/null +++ b/ump/integration_test/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ump/integration_test/Info.plist b/ump/integration_test/Info.plist new file mode 100644 index 0000000000..d2403b9c2c --- /dev/null +++ b/ump/integration_test/Info.plist @@ -0,0 +1,39 @@ + + + + + GADApplicationIdentifier + ca-app-pub-3940256099942544~1458002511 + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + CFBundleURLTypes + + + CFBundleURLSchemes + + REPLACE_WITH_REVERSED_CLIENT_ID + firebase-game-loop + firebase-ui-test + + + + + diff --git a/ump/integration_test/LaunchScreen.storyboard b/ump/integration_test/LaunchScreen.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/ump/integration_test/LaunchScreen.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ump/integration_test/LibraryManifest.xml b/ump/integration_test/LibraryManifest.xml new file mode 100644 index 0000000000..14c3c3caf5 --- /dev/null +++ b/ump/integration_test/LibraryManifest.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/ump/integration_test/Podfile b/ump/integration_test/Podfile new file mode 100644 index 0000000000..091c3f761d --- /dev/null +++ b/ump/integration_test/Podfile @@ -0,0 +1,16 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '13.0' +# Firebase UMP test application. +use_frameworks! :linkage => :static + +target 'integration_test' do + platform :ios, '13.0' + pod 'Firebase/CoreOnly', '11.14.0' + pod 'GoogleUserMessagingPlatform', '2.3.0' +end + +post_install do |installer| + # If this is running from inside the SDK directory, run the setup script. + system("if [[ -r ../../setup_integration_tests.py ]]; then python3 ../../setup_integration_tests.py .; fi") + system("python3 ./download_googletest.py") +end diff --git a/ump/integration_test/build.gradle b/ump/integration_test/build.gradle new file mode 100644 index 0000000000..d31f643652 --- /dev/null +++ b/ump/integration_test/build.gradle @@ -0,0 +1,108 @@ +// 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. + +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + mavenLocal() + maven { url 'https://maven.google.com' } + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + // r8 on this version of the Android tools has a bug, + // so specify a different version to use. + classpath 'com.android.tools:r8:8.3.37' + classpath 'com.google.gms:google-services:4.4.1' + } +} + +allprojects { + repositories { + mavenLocal() + maven { url 'https://maven.google.com' } + mavenCentral() + } +} + +apply plugin: 'com.android.application' + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + compileSdkVersion 34 + ndkPath System.getenv('ANDROID_NDK_HOME') + buildToolsVersion '32.0.0' + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/android/java'] + res.srcDirs = ['res'] + } + } + + defaultConfig { + applicationId 'com.google.android.admob.testapp' + minSdkVersion 23 + targetSdkVersion 34 + versionCode 1 + versionName '1.0' + externalNativeBuild.cmake { + arguments "-DFIREBASE_CPP_SDK_DIR=$gradle.firebase_cpp_sdk_dir" + } + multiDexEnabled true + } + externalNativeBuild.cmake { + path 'CMakeLists.txt' + } + buildTypes { + release { + minifyEnabled true + proguardFile getDefaultProguardFile('proguard-android.txt') + proguardFile file('proguard.pro') + } + } + lintOptions { + abortOnError false + } +} + +apply from: "$gradle.firebase_cpp_sdk_dir/Android/firebase_dependencies.gradle" +firebaseCpp.dependencies { + ump +} + +apply plugin: 'com.google.gms.google-services' + +task copyIntegrationTestFiles(type:Exec) { + // If this is running form inside the SDK directory, run the setup script. + if (project.file('../../setup_integration_tests.py').exists()) { + commandLine 'python3', '../../setup_integration_tests.py', project.projectDir.toString() + } + else { + commandLine 'echo', '' + } +} + +build.dependsOn(copyIntegrationTestFiles) + +project.afterEvaluate { + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} diff --git a/ump/integration_test/empty.swift b/ump/integration_test/empty.swift new file mode 100644 index 0000000000..b637790955 --- /dev/null +++ b/ump/integration_test/empty.swift @@ -0,0 +1,9 @@ +// +// empty.swift +// integration_test +// +// Created by David Della Bitta on 5/12/22. +// Copyright © 2022 Google. All rights reserved. +// + +import Foundation diff --git a/ump/integration_test/googletest.cmake b/ump/integration_test/googletest.cmake new file mode 100644 index 0000000000..a643a3e2f2 --- /dev/null +++ b/ump/integration_test/googletest.cmake @@ -0,0 +1,35 @@ +# 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. + +# Download GoogleTest from GitHub as an external project. +# Pin to 1.11.0 because we touch internal GoogleTest structures that could change in the future. + +# This CMake file is taken from: +# https://github.com/google/googletest/blob/master/googletest/README.md#incorporating-into-an-existing-cmake-project + +cmake_minimum_required(VERSION 2.8.2) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG "release-1.11.0" + SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/src" + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) diff --git a/ump/integration_test/gradle.properties b/ump/integration_test/gradle.properties new file mode 100644 index 0000000000..ac891ac594 --- /dev/null +++ b/ump/integration_test/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX = true +org.gradle.jvmargs=-Xmx2560m diff --git a/ump/integration_test/gradle/wrapper/gradle-wrapper.jar b/ump/integration_test/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..8c0fb64a86 Binary files /dev/null and b/ump/integration_test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/ump/integration_test/gradle/wrapper/gradle-wrapper.properties b/ump/integration_test/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..2eb04a3b17 --- /dev/null +++ b/ump/integration_test/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Nov 27 14:03:45 PST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https://services.gradle.org/distributions/gradle-7.5.1-all.zip diff --git a/ump/integration_test/gradlew b/ump/integration_test/gradlew new file mode 100755 index 0000000000..06eaee39e2 --- /dev/null +++ b/ump/integration_test/gradlew @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# +# 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. + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/ump/integration_test/gradlew.bat b/ump/integration_test/gradlew.bat new file mode 100644 index 0000000000..b23d13537c --- /dev/null +++ b/ump/integration_test/gradlew.bat @@ -0,0 +1,104 @@ +@rem Copyright 2021 Google LLC +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ump/integration_test/integration_test.xcodeproj/project.pbxproj b/ump/integration_test/integration_test.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3f0c3ff6ad --- /dev/null +++ b/ump/integration_test/integration_test.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 520BC0391C869159008CFBC3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */; }; + 529226D61C85F68000C89379 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D51C85F68000C89379 /* Foundation.framework */; }; + 529226D81C85F68000C89379 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D71C85F68000C89379 /* CoreGraphics.framework */; }; + 529226DA1C85F68000C89379 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 529226D91C85F68000C89379 /* UIKit.framework */; }; + D61C5F8E22BABA9C00A79141 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D61C5F8C22BABA9B00A79141 /* Images.xcassets */; }; + D61C5F9622BABAD200A79141 /* integration_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D61C5F9222BABAD100A79141 /* integration_test.cc */; }; + D62CCBC022F367140099BE9F /* gmock-all.cc in Sources */ = {isa = PBXBuildFile; fileRef = D62CCBBF22F367140099BE9F /* gmock-all.cc */; }; + D640F3172819C85800AC956E /* empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = D640F3162819C85800AC956E /* empty.swift */; }; + D66B16871CE46E8900E5638A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */; }; + D67D355822BABD2200292C1D /* gtest-all.cc in Sources */ = {isa = PBXBuildFile; fileRef = D67D355622BABD2100292C1D /* gtest-all.cc */; }; + D686A3292A8B16F20034845A /* AppTrackingTransparency.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */; }; + D6C179E922CB322900C2651A /* ios_app_framework.mm in Sources */ = {isa = PBXBuildFile; fileRef = D6C179E722CB322900C2651A /* ios_app_framework.mm */; }; + D6C179EA22CB322900C2651A /* ios_firebase_test_framework.mm in Sources */ = {isa = PBXBuildFile; fileRef = D6C179E822CB322900C2651A /* ios_firebase_test_framework.mm */; }; + D6C179EE22CB323300C2651A /* firebase_test_framework.cc in Sources */ = {isa = PBXBuildFile; fileRef = D6C179EC22CB323300C2651A /* firebase_test_framework.cc */; }; + D6C179F022CB32A000C2651A /* app_framework.cc in Sources */ = {isa = PBXBuildFile; fileRef = D6C179EF22CB32A000C2651A /* app_framework.cc */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 529226D21C85F68000C89379 /* integration_test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = integration_test.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 529226D51C85F68000C89379 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 529226D71C85F68000C89379 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 529226D91C85F68000C89379 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 529226EE1C85F68000C89379 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + D61C5F8C22BABA9B00A79141 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D61C5F8D22BABA9C00A79141 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D61C5F9222BABAD100A79141 /* integration_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = integration_test.cc; path = src/integration_test.cc; sourceTree = ""; }; + D62CCBBF22F367140099BE9F /* gmock-all.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "gmock-all.cc"; path = "external/googletest/src/googlemock/src/gmock-all.cc"; sourceTree = ""; }; + D62CCBC122F367320099BE9F /* gmock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gmock.h; path = external/googletest/src/googlemock/include/gmock/gmock.h; sourceTree = ""; }; + D640F3162819C85800AC956E /* empty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = empty.swift; path = src/empty.swift; sourceTree = ""; }; + D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + D67D355622BABD2100292C1D /* gtest-all.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "gtest-all.cc"; path = "external/googletest/src/googletest/src/gtest-all.cc"; sourceTree = ""; }; + D67D355722BABD2100292C1D /* gtest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = gtest.h; path = external/googletest/src/googletest/include/gtest/gtest.h; sourceTree = ""; }; + D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppTrackingTransparency.framework; path = System/Library/Frameworks/AppTrackingTransparency.framework; sourceTree = SDKROOT; }; + D6C179E722CB322900C2651A /* ios_app_framework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_app_framework.mm; path = src/ios/ios_app_framework.mm; sourceTree = ""; }; + D6C179E822CB322900C2651A /* ios_firebase_test_framework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ios_firebase_test_framework.mm; path = src/ios/ios_firebase_test_framework.mm; sourceTree = ""; }; + D6C179EB22CB323300C2651A /* firebase_test_framework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = firebase_test_framework.h; path = src/firebase_test_framework.h; sourceTree = ""; }; + D6C179EC22CB323300C2651A /* firebase_test_framework.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = firebase_test_framework.cc; path = src/firebase_test_framework.cc; sourceTree = ""; }; + D6C179ED22CB323300C2651A /* app_framework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = app_framework.h; path = src/app_framework.h; sourceTree = ""; }; + D6C179EF22CB32A000C2651A /* app_framework.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = app_framework.cc; path = src/app_framework.cc; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 529226CF1C85F68000C89379 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 529226D81C85F68000C89379 /* CoreGraphics.framework in Frameworks */, + D686A3292A8B16F20034845A /* AppTrackingTransparency.framework in Frameworks */, + 529226DA1C85F68000C89379 /* UIKit.framework in Frameworks */, + 529226D61C85F68000C89379 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 529226C91C85F68000C89379 = { + isa = PBXGroup; + children = ( + D61C5F8C22BABA9B00A79141 /* Images.xcassets */, + D61C5F8D22BABA9C00A79141 /* Info.plist */, + D66B16861CE46E8900E5638A /* LaunchScreen.storyboard */, + 520BC0381C869159008CFBC3 /* GoogleService-Info.plist */, + 5292271D1C85FB5500C89379 /* src */, + 529226D41C85F68000C89379 /* Frameworks */, + 529226D31C85F68000C89379 /* Products */, + ); + sourceTree = ""; + }; + 529226D31C85F68000C89379 /* Products */ = { + isa = PBXGroup; + children = ( + 529226D21C85F68000C89379 /* integration_test.app */, + ); + name = Products; + sourceTree = ""; + }; + 529226D41C85F68000C89379 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D686A3282A8B16F20034845A /* AppTrackingTransparency.framework */, + 529226D51C85F68000C89379 /* Foundation.framework */, + 529226D71C85F68000C89379 /* CoreGraphics.framework */, + 529226D91C85F68000C89379 /* UIKit.framework */, + 529226EE1C85F68000C89379 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5292271D1C85FB5500C89379 /* src */ = { + isa = PBXGroup; + children = ( + D640F3162819C85800AC956E /* empty.swift */, + D62CCBC122F367320099BE9F /* gmock.h */, + D62CCBBF22F367140099BE9F /* gmock-all.cc */, + D67D355622BABD2100292C1D /* gtest-all.cc */, + D67D355722BABD2100292C1D /* gtest.h */, + D6C179EF22CB32A000C2651A /* app_framework.cc */, + D6C179ED22CB323300C2651A /* app_framework.h */, + D6C179EC22CB323300C2651A /* firebase_test_framework.cc */, + D6C179EB22CB323300C2651A /* firebase_test_framework.h */, + D61C5F9222BABAD100A79141 /* integration_test.cc */, + 5292271E1C85FB5B00C89379 /* ios */, + ); + name = src; + sourceTree = ""; + }; + 5292271E1C85FB5B00C89379 /* ios */ = { + isa = PBXGroup; + children = ( + D6C179E722CB322900C2651A /* ios_app_framework.mm */, + D6C179E822CB322900C2651A /* ios_firebase_test_framework.mm */, + ); + name = ios; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 529226D11C85F68000C89379 /* integration_test */ = { + isa = PBXNativeTarget; + buildConfigurationList = 529226F91C85F68000C89379 /* Build configuration list for PBXNativeTarget "integration_test" */; + buildPhases = ( + 529226CE1C85F68000C89379 /* Sources */, + 529226CF1C85F68000C89379 /* Frameworks */, + 529226D01C85F68000C89379 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = integration_test; + productName = testapp; + productReference = 529226D21C85F68000C89379 /* integration_test.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 529226CA1C85F68000C89379 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0640; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 529226D11C85F68000C89379 = { + CreatedOnToolsVersion = 6.4; + DevelopmentTeam = EQHXZ8M8AV; + LastSwiftMigration = 1320; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 529226CD1C85F68000C89379 /* Build configuration list for PBXProject "integration_test" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = 529226C91C85F68000C89379; + productRefGroup = 529226D31C85F68000C89379 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 529226D11C85F68000C89379 /* integration_test */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 529226D01C85F68000C89379 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D61C5F8E22BABA9C00A79141 /* Images.xcassets in Resources */, + D66B16871CE46E8900E5638A /* LaunchScreen.storyboard in Resources */, + 520BC0391C869159008CFBC3 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 529226CE1C85F68000C89379 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D67D355822BABD2200292C1D /* gtest-all.cc in Sources */, + D62CCBC022F367140099BE9F /* gmock-all.cc in Sources */, + D6C179EA22CB322900C2651A /* ios_firebase_test_framework.mm in Sources */, + D61C5F9622BABAD200A79141 /* integration_test.cc in Sources */, + D6C179E922CB322900C2651A /* ios_app_framework.mm in Sources */, + D640F3172819C85800AC956E /* empty.swift in Sources */, + D6C179F022CB32A000C2651A /* app_framework.cc in Sources */, + D6C179EE22CB323300C2651A /* firebase_test_framework.cc in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 529226F71C85F68000C89379 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 529226F81C85F68000C89379 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 529226FA1C85F68000C89379 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + EXCLUDED_ARCHS = i386; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "\"$(SRCROOT)/src\"", + "\"$(SRCROOT)/external/googletest/src/googletest/include\"", + "\"$(SRCROOT)/external/googletest/src/googlemock/include\"", + "\"$(SRCROOT)/external/googletest/src/googletest\"", + "\"$(SRCROOT)/external/googletest/src/googlemock\"", + ); + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.3; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 529226FB1C85F68000C89379 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + EXCLUDED_ARCHS = i386; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "\"$(SRCROOT)/src\"", + "\"$(SRCROOT)/external/googletest/src/googletest/include\"", + "\"$(SRCROOT)/external/googletest/src/googlemock/include\"", + "\"$(SRCROOT)/external/googletest/src/googletest\"", + "\"$(SRCROOT)/external/googletest/src/googlemock\"", + ); + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.3; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 529226CD1C85F68000C89379 /* Build configuration list for PBXProject "integration_test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 529226F71C85F68000C89379 /* Debug */, + 529226F81C85F68000C89379 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 529226F91C85F68000C89379 /* Build configuration list for PBXNativeTarget "integration_test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 529226FA1C85F68000C89379 /* Debug */, + 529226FB1C85F68000C89379 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 529226CA1C85F68000C89379 /* Project object */; +} diff --git a/ump/integration_test/proguard.pro b/ump/integration_test/proguard.pro new file mode 100644 index 0000000000..2d04b8a9a5 --- /dev/null +++ b/ump/integration_test/proguard.pro @@ -0,0 +1,2 @@ +-ignorewarnings +-keep,includedescriptorclasses public class com.google.firebase.example.LoggingUtils { * ; } diff --git a/ump/integration_test/res/layout/main.xml b/ump/integration_test/res/layout/main.xml new file mode 100644 index 0000000000..54e03ce04c --- /dev/null +++ b/ump/integration_test/res/layout/main.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/ump/integration_test/res/values/strings.xml b/ump/integration_test/res/values/strings.xml new file mode 100644 index 0000000000..462d11c50c --- /dev/null +++ b/ump/integration_test/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + Firebase UMP Integration Test + diff --git a/ump/integration_test/settings.gradle b/ump/integration_test/settings.gradle new file mode 100644 index 0000000000..7e56f6228e --- /dev/null +++ b/ump/integration_test/settings.gradle @@ -0,0 +1,41 @@ +// 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. + +def firebase_cpp_sdk_dir = System.getProperty('firebase_cpp_sdk.dir') +if (firebase_cpp_sdk_dir == null || firebase_cpp_sdk_dir.isEmpty()) { + firebase_cpp_sdk_dir = System.getenv('FIREBASE_CPP_SDK_DIR') + if (firebase_cpp_sdk_dir == null || firebase_cpp_sdk_dir.isEmpty()) { + if ((file('../../cpp_sdk_version.json')).exists()) { + firebase_cpp_sdk_dir = file('../..').absolutePath + } + else if ((file('firebase_cpp_sdk')).exists()) { + firebase_cpp_sdk_dir = 'firebase_cpp_sdk' + } else { + throw new StopActionException( + 'firebase_cpp_sdk.dir property or the FIREBASE_CPP_SDK_DIR ' + + 'environment variable must be set to reference the Firebase C++ ' + + 'SDK install directory. This is used to configure static library ' + + 'and C/C++ include paths for the SDK.') + } + } +} +if (!(new File(firebase_cpp_sdk_dir)).exists()) { + throw new StopActionException( + sprintf('Firebase C++ SDK directory %s does not exist', + firebase_cpp_sdk_dir)) +} +gradle.ext.firebase_cpp_sdk_dir = "$firebase_cpp_sdk_dir" +includeBuild("$firebase_cpp_sdk_dir") { + name = "firebase_cpp_sdk" +} diff --git a/ump/integration_test/src/integration_test.cc b/ump/integration_test/src/integration_test.cc new file mode 100644 index 0000000000..97ff0c3976 --- /dev/null +++ b/ump/integration_test/src/integration_test.cc @@ -0,0 +1,836 @@ +// Copyright 2021 Google LLC. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "app_framework.h" // NOLINT +#include "firebase/app.h" +#include "firebase/ump.h" +#include "firebase/util.h" +#include "firebase_test_framework.h" // NOLINT + +#if defined(ANDROID) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) +// includes for phone-only tests. +#include +#include +#endif // defined(ANDROID) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) + +// The TO_STRING macro is useful for command line defined strings as the quotes +// get stripped. +#define TO_STRING_EXPAND(X) #X +#define TO_STRING(X) TO_STRING_EXPAND(X) + +// Path to the Firebase config file to load. +#ifdef FIREBASE_CONFIG +#define FIREBASE_CONFIG_STRING TO_STRING(FIREBASE_CONFIG) +#else +#define FIREBASE_CONFIG_STRING "" +#endif // FIREBASE_CONFIG + +namespace firebase_testapp_automated { + +// Sample test device IDs to use in making the request. +// You can replace these with actual device IDs for UMP tests +// to work on hardware devices. +const std::vector kTestDeviceIDs = { + "2077ef9a63d2b398840261c8221a0c9b", "098fe087d987c9a878965454a65654d7"}; + +using app_framework::LogDebug; +using app_framework::LogInfo; +using app_framework::LogWarning; +using app_framework::ProcessEvents; + +using firebase_test_framework::FirebaseTest; + +using testing::AnyOf; +using testing::Contains; +using testing::ElementsAre; +using testing::Eq; +using testing::HasSubstr; +using testing::Pair; +using testing::Property; + +class FirebaseUmpTest : public FirebaseTest { + public: + FirebaseUmpTest() : consent_info_(nullptr) {} + + // Whether to call ConsentInfo::Reset() upon initialization, which + // resets UMP's consent state to as if the app was first installed. + enum ResetOption { kReset, kNoReset }; + + void InitializeUmp(ResetOption reset = kReset); + void TerminateUmp(ResetOption reset = kReset); + + static void SetUpTestSuite(); + static void TearDownTestSuite(); + + void SetUp() override; + void TearDown() override; + + protected: + static firebase::App* shared_app_; + firebase::ump::ConsentInfo* consent_info_; +}; + +firebase::App* FirebaseUmpTest::shared_app_ = nullptr; + +void FirebaseUmpTest::SetUpTestSuite() { + LogDebug("Initialize Firebase App."); + + FindFirebaseConfig(FIREBASE_CONFIG_STRING); + +#if defined(ANDROID) + shared_app_ = ::firebase::App::Create(app_framework::GetJniEnv(), + app_framework::GetActivity()); +#else + shared_app_ = ::firebase::App::Create(); +#endif // defined(ANDROID) +} + +void FirebaseUmpTest::TearDownTestSuite() { + LogDebug("Shutdown Firebase App."); + delete shared_app_; + shared_app_ = nullptr; +} + +void FirebaseUmpTest::InitializeUmp(ResetOption reset) { + using firebase::ump::ConsentInfo; + firebase::InitResult result; + consent_info_ = ConsentInfo::GetInstance(*shared_app_, &result); + + EXPECT_NE(consent_info_, nullptr); + EXPECT_EQ(result, firebase::kInitResultSuccess); + + if (consent_info_ != nullptr && reset == kReset) { + consent_info_->Reset(); + } +} + +void FirebaseUmpTest::TerminateUmp(ResetOption reset) { + if (consent_info_) { + if (reset == kReset) { + consent_info_->Reset(); + } + delete consent_info_; + consent_info_ = nullptr; + } +} + +void FirebaseUmpTest::SetUp() { + InitializeUmp(); + ASSERT_NE(consent_info_, nullptr); +} + +void FirebaseUmpTest::TearDown() { TerminateUmp(); } + +// Tests for User Messaging Platform +TEST_F(FirebaseUmpTest, TestUmpInitialization) { + // Initialize handled automatically in test setup. + EXPECT_NE(consent_info_, nullptr); + // Terminate handled automatically in test teardown. +} + +// Tests for User Messaging Platform +TEST_F(FirebaseUmpTest, TestUmpDefaultsToUnknownStatus) { + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusUnknown); + EXPECT_EQ(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusUnknown); + EXPECT_EQ(consent_info_->GetPrivacyOptionsRequirementStatus(), + firebase::ump::kPrivacyOptionsRequirementStatusUnknown); + EXPECT_FALSE(consent_info_->CanRequestAds()); +} + +// Tests for User Messaging Platform +TEST_F(FirebaseUmpTest, TestUmpGetInstanceIsAlwaysEqual) { + using firebase::ump::ConsentInfo; + + EXPECT_NE(consent_info_, nullptr); + + // Ensure that GetInstance() with any options is always equal. + EXPECT_EQ(consent_info_, ConsentInfo::GetInstance()); + EXPECT_EQ(consent_info_, ConsentInfo::GetInstance(*shared_app_)); + +#if defined(ANDROID) + EXPECT_EQ(consent_info_, + ConsentInfo::GetInstance(app_framework::GetJniEnv(), + app_framework::GetActivity())); + + firebase::App* second_app = firebase::App::Create( + firebase::AppOptions(), "2ndApp", app_framework::GetJniEnv(), + app_framework::GetActivity()); +#else + firebase::App* second_app = + firebase::App::Create(firebase::AppOptions(), "2ndApp"); +#endif // defined(ANDROID) + + EXPECT_EQ(consent_info_, ConsentInfo::GetInstance(*second_app)); + + delete second_app; +} + +TEST_F(FirebaseUmpTest, TestUmpRequestConsentInfoUpdate) { + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + EXPECT_TRUE(future == consent_info_->RequestConsentInfoUpdateLastResult()); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + EXPECT_NE(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusUnknown); + EXPECT_NE(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusUnknown); + EXPECT_NE(consent_info_->GetPrivacyOptionsRequirementStatus(), + firebase::ump::kPrivacyOptionsRequirementStatusUnknown); +} + +TEST_F(FirebaseUmpTest, TestUmpRequestConsentInfoUpdateDebugEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); +} + +TEST_F(FirebaseUmpTest, TestUmpRequestConsentInfoUpdateDebugNonEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + EXPECT_THAT(consent_info_->GetConsentStatus(), + AnyOf(Eq(firebase::ump::kConsentStatusNotRequired), + Eq(firebase::ump::kConsentStatusRequired))); +} + +TEST_F(FirebaseUmpTest, TestUmpLoadForm) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); + + EXPECT_EQ(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusAvailable); + + // Load the form. Run this step with retry in case of network timeout. + WaitForCompletion( + RunWithRetry([&]() { return consent_info_->LoadConsentForm(); }), + "LoadConsentForm", + {firebase::ump::kConsentFormSuccess, + firebase::ump::kConsentFormErrorTimeout}); + + firebase::Future future = consent_info_->LoadConsentFormLastResult(); + + EXPECT_EQ(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusAvailable); + + if (future.error() == firebase::ump::kConsentFormErrorTimeout) { + LogWarning("Timed out after multiple tries, but passing anyway."); + } +} + +TEST_F(FirebaseUmpTest, TestUmpShowForm) { + TEST_REQUIRES_USER_INTERACTION; + + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); + + EXPECT_EQ(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusAvailable); + + WaitForCompletion(consent_info_->LoadConsentForm(), "LoadConsentForm"); + + EXPECT_EQ(consent_info_->GetConsentFormStatus(), + firebase::ump::kConsentFormStatusAvailable); + + firebase::Future future = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + + EXPECT_TRUE(future == consent_info_->ShowConsentFormLastResult()); + + WaitForCompletion(future, "ShowConsentForm"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusObtained); +} + +TEST_F(FirebaseUmpTest, TestUmpLoadFormUnderAgeOfConsent) { + SKIP_TEST_ON_IOS_SIMULATOR; + + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = true; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + firebase::Future load_future = consent_info_->LoadConsentForm(); + WaitForCompletion(load_future, "LoadConsentForm", + {firebase::ump::kConsentFormErrorUnavailable, + firebase::ump::kConsentFormErrorTimeout, + firebase::ump::kConsentFormSuccess}); +} + +TEST_F(FirebaseUmpTest, TestUmpLoadFormUnavailableDebugNonEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + if (consent_info_->GetConsentStatus() != + firebase::ump::kConsentStatusRequired) { + WaitForCompletion(consent_info_->LoadConsentForm(), "LoadConsentForm", + firebase::ump::kConsentFormErrorUnavailable); + } +} + +TEST_F(FirebaseUmpTest, TestUmpLoadAndShowIfRequiredDebugNonEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future = + consent_info_->RequestConsentInfoUpdate(params); + + WaitForCompletion(future, "RequestConsentInfoUpdate", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + // Retry only network errors. + EXPECT_NE(future.error(), firebase::ump::kConsentRequestErrorNetwork); + + FLAKY_TEST_SECTION_END(); + + EXPECT_THAT(consent_info_->GetConsentStatus(), + AnyOf(Eq(firebase::ump::kConsentStatusNotRequired), + Eq(firebase::ump::kConsentStatusRequired))); + + if (consent_info_->GetConsentStatus() == + firebase::ump::kConsentStatusNotRequired || + ShouldRunUITests()) { + // If ConsentStatus is Required, we only want to do this next part if UI + // interaction is allowed, as it will show a consent form which won't work + // in automated testing. + firebase::Future future = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + + EXPECT_TRUE(future == + consent_info_->LoadAndShowConsentFormIfRequiredLastResult()); + + WaitForCompletion(future, "LoadAndShowConsentFormIfRequired"); + } +} + +TEST_F(FirebaseUmpTest, TestUmpLoadAndShowIfRequiredDebugEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + TEST_REQUIRES_USER_INTERACTION; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); + + firebase::Future future = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + + EXPECT_TRUE(future == + consent_info_->LoadAndShowConsentFormIfRequiredLastResult()); + + WaitForCompletion(future, "LoadAndShowConsentFormIfRequired"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusObtained); +} + +TEST_F(FirebaseUmpTest, TestUmpPrivacyOptions) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + using firebase::ump::PrivacyOptionsRequirementStatus; + + TEST_REQUIRES_USER_INTERACTION; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); + + EXPECT_FALSE(consent_info_->CanRequestAds()); + + WaitForCompletion(consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()), + "LoadAndShowConsentFormIfRequired"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusObtained); + + EXPECT_TRUE(consent_info_->CanRequestAds()) << "After consent obtained"; + + LogInfo( + "******** On the Privacy Options screen that is about to appear, please " + "select DO NOT CONSENT."); + + ProcessEvents(5000); + + EXPECT_EQ(consent_info_->GetPrivacyOptionsRequirementStatus(), + firebase::ump::kPrivacyOptionsRequirementStatusRequired); + + firebase::Future future = consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + + EXPECT_TRUE(future == consent_info_->ShowPrivacyOptionsFormLastResult()); + + WaitForCompletion(future, "ShowPrivacyOptionsForm"); +} + +TEST_F(FirebaseUmpTest, TestCanRequestAdsNonEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_THAT(consent_info_->GetConsentStatus(), + AnyOf(Eq(firebase::ump::kConsentStatusNotRequired), + Eq(firebase::ump::kConsentStatusRequired))); + + if (consent_info_->GetConsentStatus() == + firebase::ump::kConsentStatusNotRequired) { + EXPECT_TRUE(consent_info_->CanRequestAds()); + } +} + +TEST_F(FirebaseUmpTest, TestCanRequestAdsEEA) { + using firebase::ump::ConsentDebugSettings; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + EXPECT_EQ(consent_info_->GetConsentStatus(), + firebase::ump::kConsentStatusRequired); + + EXPECT_FALSE(consent_info_->CanRequestAds()); +} + +TEST_F(FirebaseUmpTest, TestUmpCleanupWithDelay) { + // Ensure that if ConsentInfo is deleted after a delay, Futures are + // properly invalidated. + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future_request = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_load = consent_info_->LoadConsentForm(); + firebase::Future future_show = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + firebase::Future future_load_and_show = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + firebase::Future future_privacy = consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + + ProcessEvents(5000); + + TerminateUmp(kNoReset); + + EXPECT_EQ(future_request.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_load.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_show.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_load_and_show.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_privacy.status(), firebase::kFutureStatusInvalid); +} + +TEST_F(FirebaseUmpTest, TestUmpCleanupRaceCondition) { + // Ensure that if ConsentInfo is deleted immediately, operations + // (and their Futures) are properly invalidated. + + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future_request = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_load = consent_info_->LoadConsentForm(); + firebase::Future future_show = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + firebase::Future future_load_and_show = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + firebase::Future future_privacy = consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + + TerminateUmp(kNoReset); + + EXPECT_EQ(future_request.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_load.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_show.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_load_and_show.status(), firebase::kFutureStatusInvalid); + EXPECT_EQ(future_privacy.status(), firebase::kFutureStatusInvalid); + + ProcessEvents(5000); +} + +TEST_F(FirebaseUmpTest, TestUmpCallbacksOnWrongInstance) { + // Ensure that if ConsentInfo is deleted and then recreated, stale + // callbacks don't call into the new instance and cause crashes. + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + LogDebug("RequestConsentInfoUpdate"); + consent_info_->RequestConsentInfoUpdate(params).OnCompletion( + [](const firebase::Future&) { + LogDebug("RequestConsentInfoUpdate done"); + }); + LogDebug("LoadConsentForm"); + consent_info_->LoadConsentForm().OnCompletion( + [](const firebase::Future&) { LogDebug("LoadConsentForm done"); }); + // In automated tests, only check RequestConsentInfoUpdate and LoadConsentForm + // as the rest may show UI. + if (ShouldRunUITests()) { + LogDebug("ShowConsentForm"); + consent_info_->ShowConsentForm(app_framework::GetWindowController()) + .OnCompletion([](const firebase::Future&) { + LogDebug("ShowConsentForm done"); + }); + LogDebug("LoadAndShowConsentFormIfRequired"); + consent_info_ + ->LoadAndShowConsentFormIfRequired(app_framework::GetWindowController()) + .OnCompletion([](const firebase::Future&) { + LogDebug("LoadAndShowConsentFormIfRequired done"); + }); + LogDebug("ShowPrivacyOptionsForm"); + consent_info_->ShowPrivacyOptionsForm(app_framework::GetWindowController()) + .OnCompletion([](const firebase::Future&) { + LogDebug("ShowPrivacyOptionsForm done"); + }); + } + + LogDebug("Terminate"); + TerminateUmp(kNoReset); + + LogDebug("Initialize"); + InitializeUmp(kNoReset); + + // Give the operations time to complete. + LogDebug("Wait"); + ProcessEvents(5000); + + LogDebug("Done"); +} + +TEST_F(FirebaseUmpTest, TestUmpMethodsReturnOperationInProgress) { + SKIP_TEST_ON_DESKTOP; + SKIP_TEST_ON_IOS_SIMULATOR; // LoadAndShowConsentFormIfRequired + // is too quick on simulator. + + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + // Check that all of the UMP operations properly return an OperationInProgress + // error if called more than once at the same time. + + // This depends on timing, so it's inherently flaky. + FLAKY_TEST_SECTION_BEGIN(); + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyNonEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future_request_1 = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_request_2 = + consent_info_->RequestConsentInfoUpdate(params); + WaitForCompletion(future_request_2, "RequestConsentInfoUpdate second", + firebase::ump::kConsentRequestErrorOperationInProgress); + WaitForCompletion(future_request_1, "RequestConsentInfoUpdate first", + {firebase::ump::kConsentRequestSuccess, + firebase::ump::kConsentRequestErrorNetwork}); + + consent_info_->Reset(); + + FLAKY_TEST_SECTION_END(); +} + +TEST_F(FirebaseUmpTest, TestUmpMethodsReturnOperationInProgressWithUI) { + SKIP_TEST_ON_DESKTOP; + TEST_REQUIRES_USER_INTERACTION; + + using firebase::ump::ConsentFormStatus; + using firebase::ump::ConsentRequestParameters; + using firebase::ump::ConsentStatus; + + // Check that all of the UMP operations properly return an OperationInProgress + // error if called more than once at the same time. This test include methods + // with UI interaction. + + ConsentRequestParameters params; + params.tag_for_under_age_of_consent = false; + params.debug_settings.debug_geography = + firebase::ump::kConsentDebugGeographyEEA; + params.debug_settings.debug_device_ids = kTestDeviceIDs; + params.debug_settings.debug_device_ids.push_back(GetDebugDeviceId()); + + firebase::Future future_request_1 = + consent_info_->RequestConsentInfoUpdate(params); + firebase::Future future_request_2 = + consent_info_->RequestConsentInfoUpdate(params); + WaitForCompletion(future_request_2, "RequestConsentInfoUpdate second", + firebase::ump::kConsentRequestErrorOperationInProgress); + WaitForCompletion(future_request_1, "RequestConsentInfoUpdate first"); + + firebase::Future future_load_1 = consent_info_->LoadConsentForm(); + firebase::Future future_load_2 = consent_info_->LoadConsentForm(); + WaitForCompletion(future_load_2, "LoadConsentForm second", + firebase::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_load_1, "LoadConsentForm first"); + + firebase::Future future_show_1 = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + firebase::Future future_show_2 = + consent_info_->ShowConsentForm(app_framework::GetWindowController()); + WaitForCompletion(future_show_2, "ShowConsentForm second", + firebase::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_show_1, "ShowConsentForm first"); + + firebase::Future future_privacy_1 = + consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + firebase::Future future_privacy_2 = + consent_info_->ShowPrivacyOptionsForm( + app_framework::GetWindowController()); + WaitForCompletion(future_privacy_2, "ShowPrivacyOptionsForm second", + firebase::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_privacy_1, "ShowPrivacyOptionsForm first"); + + consent_info_->Reset(); + // Request again so we can test LoadAndShowConsentFormIfRequired. + WaitForCompletion(consent_info_->RequestConsentInfoUpdate(params), + "RequestConsentInfoUpdate"); + + firebase::Future future_load_and_show_1 = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + firebase::Future future_load_and_show_2 = + consent_info_->LoadAndShowConsentFormIfRequired( + app_framework::GetWindowController()); + WaitForCompletion(future_load_and_show_2, + "LoadAndShowConsentFormIfRequired second", + firebase::ump::kConsentFormErrorOperationInProgress); + WaitForCompletion(future_load_and_show_1, + "LoadAndShowConsentFormIfRequired first"); +} + +} // namespace firebase_testapp_automated diff --git a/ump/src/android/consent_info_internal_android.cc b/ump/src/android/consent_info_internal_android.cc new file mode 100644 index 0000000000..93b8ae05bb --- /dev/null +++ b/ump/src/android/consent_info_internal_android.cc @@ -0,0 +1,669 @@ +/* + * 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. + */ + +#include "ump/src/android/consent_info_internal_android.h" + +#include + +#include +#include + +#include "app/src/assert.h" +#include "app/src/thread.h" +#include "app/src/util_android.h" +#include "firebase/internal/common.h" +#include "ump/ump_resources.h" + +namespace firebase { +namespace ump { +namespace internal { + +ConsentInfoInternalAndroid* ConsentInfoInternalAndroid::s_instance = nullptr; +firebase::Mutex ConsentInfoInternalAndroid::s_instance_mutex; + +::firebase::Mutex g_cached_ump_embedded_files_mutex; +std::vector<::firebase::internal::EmbeddedFile>* g_cached_ump_embedded_files = + nullptr; + +// clang-format off +#define CONSENTINFOHELPER_METHODS(X) \ + X(Constructor, "", "(JLandroid/app/Activity;)V"), \ + X(GetConsentStatus, "getConsentStatus", "()I"), \ + X(RequestConsentInfoUpdate, "requestConsentInfoUpdate", \ + "(JZILjava/util/ArrayList;)V"), \ + X(LoadConsentForm, "loadConsentForm", "(J)V"), \ + X(ShowConsentForm, "showConsentForm", "(JLandroid/app/Activity;)Z"), \ + X(LoadAndShowConsentFormIfRequired, "loadAndShowConsentFormIfRequired", \ + "(JLandroid/app/Activity;)V"), \ + X(GetPrivacyOptionsRequirementStatus, "getPrivacyOptionsRequirementStatus", \ + "()I"), \ + X(ShowPrivacyOptionsForm, "showPrivacyOptionsForm", \ + "(JLandroid/app/Activity;)V"), \ + X(Reset, "reset", "()V"), \ + X(CanRequestAds, "canRequestAds", "()Z"), \ + X(IsConsentFormAvailable, "isConsentFormAvailable", "()Z"), \ + X(Disconnect, "disconnect", "()V") +// clang-format on + +// clang-format off +#define CONSENTINFOHELPER_FIELDS(X) \ + X(PrivacyOptionsRequirementUnknown, \ + "PRIVACY_OPTIONS_REQUIREMENT_UNKNOWN", "I", util::kFieldTypeStatic), \ + X(PrivacyOptionsRequirementRequired, \ + "PRIVACY_OPTIONS_REQUIREMENT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(PrivacyOptionsRequirementNotRequired, \ + "PRIVACY_OPTIONS_REQUIREMENT_NOT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(FunctionRequestConsentInfoUpdate, \ + "FUNCTION_REQUEST_CONSENT_INFO_UPDATE", "I", util::kFieldTypeStatic), \ + X(FunctionLoadConsentForm, \ + "FUNCTION_LOAD_CONSENT_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionShowConsentForm, \ + "FUNCTION_SHOW_CONSENT_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionLoadAndShowConsentFormIfRequired, \ + "FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED", \ + "I", util::kFieldTypeStatic), \ + X(FunctionShowPrivacyOptionsForm, \ + "FUNCTION_SHOW_PRIVACY_OPTIONS_FORM", "I", util::kFieldTypeStatic), \ + X(FunctionCount, "FUNCTION_COUNT", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consent_info_helper, CONSENTINFOHELPER_METHODS, + CONSENTINFOHELPER_FIELDS); + +METHOD_LOOKUP_DEFINITION( + consent_info_helper, + "com/google/firebase/ump/internal/cpp/ConsentInfoHelper", + CONSENTINFOHELPER_METHODS, CONSENTINFOHELPER_FIELDS); + +// clang-format off +#define CONSENTINFORMATION_CONSENTSTATUS_FIELDS(X) \ + X(Unknown, "UNKNOWN", "I", util::kFieldTypeStatic), \ + X(NotRequired, "NOT_REQUIRED", "I", util::kFieldTypeStatic), \ + X(Required, "REQUIRED", "I", util::kFieldTypeStatic), \ + X(Obtained, "OBTAINED", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consentinformation_consentstatus, METHOD_LOOKUP_NONE, + CONSENTINFORMATION_CONSENTSTATUS_FIELDS); +METHOD_LOOKUP_DEFINITION( + consentinformation_consentstatus, + PROGUARD_KEEP_CLASS + "com/google/android/ump/ConsentInformation$ConsentStatus", + METHOD_LOOKUP_NONE, CONSENTINFORMATION_CONSENTSTATUS_FIELDS); + +// clang-format off +#define FORMERROR_ERRORCODE_FIELDS(X) \ + X(InternalError, "INTERNAL_ERROR", "I", util::kFieldTypeStatic), \ + X(InternetError, "INTERNET_ERROR", "I", util::kFieldTypeStatic), \ + X(InvalidOperation, "INVALID_OPERATION", "I", util::kFieldTypeStatic), \ + X(TimeOut, "TIME_OUT", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(formerror_errorcode, METHOD_LOOKUP_NONE, + FORMERROR_ERRORCODE_FIELDS); +METHOD_LOOKUP_DEFINITION(formerror_errorcode, + PROGUARD_KEEP_CLASS + "com/google/android/ump/FormError$ErrorCode", + METHOD_LOOKUP_NONE, FORMERROR_ERRORCODE_FIELDS); + +// clang-format off +#define CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS(X) \ + X(Disabled, "DEBUG_GEOGRAPHY_DISABLED", "I", util::kFieldTypeStatic), \ + X(EEA, "DEBUG_GEOGRAPHY_EEA", "I", util::kFieldTypeStatic), \ + X(NotEEA, "DEBUG_GEOGRAPHY_NOT_EEA", "I", util::kFieldTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(consentdebugsettings_debuggeography, + METHOD_LOOKUP_NONE, + CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS); +METHOD_LOOKUP_DEFINITION( + consentdebugsettings_debuggeography, + PROGUARD_KEEP_CLASS + "com/google/android/ump/ConsentDebugSettings$DebugGeography", + METHOD_LOOKUP_NONE, CONSENTDEBUGSETTINGS_DEBUGGEOGRAPHY_FIELDS); + +// This explicitly implements the constructor for the outer class, +// ConsentInfoInternal. +ConsentInfoInternal* ConsentInfoInternal::CreateInstance(JNIEnv* jni_env, + jobject activity) { + ConsentInfoInternalAndroid* ptr = + new ConsentInfoInternalAndroid(jni_env, activity); + if (!ptr->valid()) { + delete ptr; + return nullptr; + } + return ptr; +} + +static void ReleaseClasses(JNIEnv* env) { + consent_info_helper::ReleaseClass(env); + consentinformation_consentstatus::ReleaseClass(env); + formerror_errorcode::ReleaseClass(env); + consentdebugsettings_debuggeography::ReleaseClass(env); +} + +ConsentInfoInternalAndroid::~ConsentInfoInternalAndroid() { + JNIEnv* env = GetJNIEnv(); + env->CallVoidMethod(helper_, consent_info_helper::GetMethodId( + consent_info_helper::kDisconnect)); + + MutexLock lock(s_instance_mutex); + s_instance = nullptr; + + env->DeleteGlobalRef(helper_); + helper_ = nullptr; + + ReleaseClasses(env); + util::Terminate(env); + + env->DeleteGlobalRef(activity_); + activity_ = nullptr; + java_vm_ = nullptr; +} + +// clang-format off +#define ENUM_VALUE(class_namespace, field_name) \ + env->GetStaticIntField(class_namespace::GetClass(), \ + class_namespace::GetFieldId(class_namespace::k##field_name)) +// clang-format on + +void ConsentInfoInternalAndroid::CacheEnumValues(JNIEnv* env) { + // Cache enum values when the class loads, to avoid JNI lookups during + // callbacks later on when converting enums between Android and C++ values. + enums_.consentstatus_unknown = + ENUM_VALUE(consentinformation_consentstatus, Unknown); + enums_.consentstatus_required = + ENUM_VALUE(consentinformation_consentstatus, Required); + enums_.consentstatus_not_required = + ENUM_VALUE(consentinformation_consentstatus, NotRequired); + enums_.consentstatus_obtained = + ENUM_VALUE(consentinformation_consentstatus, Obtained); + + enums_.debug_geography_disabled = + ENUM_VALUE(consentdebugsettings_debuggeography, Disabled); + enums_.debug_geography_eea = + ENUM_VALUE(consentdebugsettings_debuggeography, EEA); + enums_.debug_geography_not_eea = + ENUM_VALUE(consentdebugsettings_debuggeography, NotEEA); + + enums_.formerror_success = 0; + enums_.formerror_internal = ENUM_VALUE(formerror_errorcode, InternalError); + enums_.formerror_network = ENUM_VALUE(formerror_errorcode, InternetError); + enums_.formerror_invalid_operation = + ENUM_VALUE(formerror_errorcode, InvalidOperation); + enums_.formerror_timeout = ENUM_VALUE(formerror_errorcode, TimeOut); + + enums_.privacy_options_requirement_unknown = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementUnknown); + enums_.privacy_options_requirement_required = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementRequired); + enums_.privacy_options_requirement_not_required = + ENUM_VALUE(consent_info_helper, PrivacyOptionsRequirementNotRequired); + + enums_.function_request_consent_info_update = + ENUM_VALUE(consent_info_helper, FunctionRequestConsentInfoUpdate); + enums_.function_load_consent_form = + ENUM_VALUE(consent_info_helper, FunctionLoadConsentForm); + enums_.function_show_consent_form = + ENUM_VALUE(consent_info_helper, FunctionShowConsentForm); + enums_.function_load_and_show_consent_form_if_required = + ENUM_VALUE(consent_info_helper, FunctionLoadAndShowConsentFormIfRequired); + enums_.function_show_privacy_options_form = + ENUM_VALUE(consent_info_helper, FunctionShowPrivacyOptionsForm); + enums_.function_count = ENUM_VALUE(consent_info_helper, FunctionCount); +} + +void ConsentInfoInternalAndroid::JNI_ConsentInfoHelper_completeFuture( + JNIEnv* env, jclass clazz, jint future_fn, jlong consent_info_internal_ptr, + jlong future_handle, jint error_code, jobject error_message_obj) { + MutexLock lock(s_instance_mutex); + if (consent_info_internal_ptr == 0 || s_instance == nullptr) { + // Calling this with a null pointer, or if there is no active + // instance, is a no-op, so just return. + return; + } + ConsentInfoInternalAndroid* instance = + reinterpret_cast(consent_info_internal_ptr); + if (s_instance != instance) { + // If the instance we were called with does not match the current + // instance, a bad race condition has occurred (whereby while waiting for + // the operation to complete, ConsentInfo was deleted and then recreated). + // In that case, fully ignore this callback. + return; + } + std::string error_message = + error_message_obj ? util::JniStringToString(env, error_message_obj) : ""; + instance->CompleteFutureFromJniCallback( + env, future_fn, static_cast(future_handle), + static_cast(error_code), + error_message.length() > 0 ? error_message.c_str() : nullptr); +} + +ConsentInfoInternalAndroid::ConsentInfoInternalAndroid(JNIEnv* env, + jobject activity) + : java_vm_(nullptr), + activity_(nullptr), + helper_(nullptr), + has_requested_consent_info_update_(false) { + MutexLock lock(s_instance_mutex); + FIREBASE_ASSERT(s_instance == nullptr); + s_instance = this; + + util::Initialize(env, activity); + env->GetJavaVM(&java_vm_); + + // Ensure we only load these files once. + { + MutexLock lock( + ::firebase::ump::internal::g_cached_ump_embedded_files_mutex); + if (::firebase::ump::internal::g_cached_ump_embedded_files == nullptr) { + ::firebase::ump::internal::g_cached_ump_embedded_files = + new std::vector(); + *::firebase::ump::internal::g_cached_ump_embedded_files = + util::CacheEmbeddedFiles(env, activity, + firebase::internal::EmbeddedFile::ToVector( + firebase_ump::ump_resources_filename, + firebase_ump::ump_resources_data, + firebase_ump::ump_resources_size)); + } + } + const std::vector& embedded_files = + *::firebase::ump::internal::g_cached_ump_embedded_files; + + if (!(consent_info_helper::CacheClassFromFiles(env, activity, + &embedded_files) != nullptr && + consent_info_helper::CacheMethodIds(env, activity) && + consent_info_helper::CacheFieldIds(env, activity) && + consentinformation_consentstatus::CacheFieldIds(env, activity) && + formerror_errorcode::CacheFieldIds(env, activity) && + consentdebugsettings_debuggeography::CacheFieldIds(env, activity))) { + ReleaseClasses(env); + util::Terminate(env); + return; + } + static const JNINativeMethod kConsentInfoHelperNativeMethods[] = { + {"completeFuture", "(IJJILjava/lang/String;)V", + reinterpret_cast(&JNI_ConsentInfoHelper_completeFuture)}}; + if (!consent_info_helper::RegisterNatives( + env, kConsentInfoHelperNativeMethods, + FIREBASE_ARRAYSIZE(kConsentInfoHelperNativeMethods))) { + util::CheckAndClearJniExceptions(env); + ReleaseClasses(env); + util::Terminate(env); + return; + } + util::CheckAndClearJniExceptions(env); + jobject helper_ref = env->NewObject( + consent_info_helper::GetClass(), + consent_info_helper::GetMethodId(consent_info_helper::kConstructor), + reinterpret_cast(this), activity); + util::CheckAndClearJniExceptions(env); + if (!helper_ref) { + ReleaseClasses(env); + util::Terminate(env); + return; + } + + helper_ = env->NewGlobalRef(helper_ref); + FIREBASE_ASSERT(helper_); + env->DeleteLocalRef(helper_ref); + + activity_ = env->NewGlobalRef(activity); + + util::CheckAndClearJniExceptions(env); + + CacheEnumValues(env); + + util::CheckAndClearJniExceptions(env); +} + +ConsentStatus ConsentInfoInternalAndroid::CppConsentStatusFromAndroid( + jint status) { + if (status == enums().consentstatus_unknown) return kConsentStatusUnknown; + if (status == enums().consentstatus_required) return kConsentStatusRequired; + if (status == enums().consentstatus_not_required) + return kConsentStatusNotRequired; + if (status == enums().consentstatus_obtained) return kConsentStatusObtained; + LogWarning("UMP: Unknown ConsentStatus returned by UMP Android SDK: %d", + (int)status); + return kConsentStatusUnknown; +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalAndroid::CppPrivacyOptionsRequirementStatusFromAndroid( + jint status) { + if (status == enums().privacy_options_requirement_unknown) + return kPrivacyOptionsRequirementStatusUnknown; + if (status == enums().privacy_options_requirement_required) + return kPrivacyOptionsRequirementStatusRequired; + if (status == enums().privacy_options_requirement_not_required) + return kPrivacyOptionsRequirementStatusNotRequired; + LogWarning( + "UMP: Unknown PrivacyOptionsRequirementStatus returned by UMP Android " + "SDK: %d", + (int)status); + return kPrivacyOptionsRequirementStatusUnknown; +} + +jint ConsentInfoInternalAndroid::AndroidDebugGeographyFromCppDebugGeography( + ConsentDebugGeography geo) { + switch (geo) { + case kConsentDebugGeographyDisabled: + return enums().debug_geography_disabled; + case kConsentDebugGeographyEEA: + return enums().debug_geography_eea; + case kConsentDebugGeographyNonEEA: + return enums().debug_geography_not_eea; + default: + return enums().debug_geography_disabled; + } +} + +// Android uses FormError to report request errors as well. +ConsentRequestError +ConsentInfoInternalAndroid::CppConsentRequestErrorFromAndroidFormError( + jint error, const char* message) { + if (error == enums().formerror_success) return kConsentRequestSuccess; + if (error == enums().formerror_internal) return kConsentRequestErrorInternal; + if (error == enums().formerror_network) return kConsentRequestErrorNetwork; + if (error == enums().formerror_invalid_operation) { + // Error strings taken directly from the UMP Android SDK. + if (message && strcasestr(message, "misconfiguration") != nullptr) + return kConsentRequestErrorMisconfiguration; + else if (message && + strcasestr(message, "requires a valid application ID") != nullptr) + return kConsentRequestErrorInvalidAppId; + else + return kConsentRequestErrorInvalidOperation; + } + LogWarning("UMP: Unknown RequestError returned by UMP Android SDK: %d (%s)", + (int)error, message ? message : ""); + return kConsentRequestErrorUnknown; +} + +ConsentFormError +ConsentInfoInternalAndroid::CppConsentFormErrorFromAndroidFormError( + jint error, const char* message) { + if (error == enums().formerror_success) return kConsentFormSuccess; + if (error == enums().formerror_internal) return kConsentFormErrorInternal; + if (error == enums().formerror_timeout) return kConsentFormErrorTimeout; + if (error == enums().formerror_invalid_operation) { + // Error strings taken directly from the UMP Android SDK. + if (message && strcasestr(message, "no available form") != nullptr) + return kConsentFormErrorUnavailable; + else if (message && strcasestr(message, "form is not required") != nullptr) + return kConsentFormErrorUnavailable; + else if (message && + strcasestr(message, "can only be invoked once") != nullptr) + return kConsentFormErrorAlreadyUsed; + else + return kConsentFormErrorInvalidOperation; + } + LogWarning("UMP: Unknown RequestError returned by UMP Android SDK: %d (%s)", + (int)error, message ? message : ""); + return kConsentFormErrorUnknown; +} + +Future ConsentInfoInternalAndroid::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + if (RequestConsentInfoUpdateLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentRequestErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); + JNIEnv* env = GetJNIEnv(); + + jlong future_handle = static_cast(handle.get().id()); + jboolean tag_for_under_age_of_consent = + params.tag_for_under_age_of_consent ? JNI_TRUE : JNI_FALSE; + jint debug_geography = AndroidDebugGeographyFromCppDebugGeography( + params.debug_settings.debug_geography); + jobject debug_device_ids_list = + util::StdVectorToJavaList(env, params.debug_settings.debug_device_ids); + env->CallVoidMethod(helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kRequestConsentInfoUpdate), + future_handle, tag_for_under_age_of_consent, + debug_geography, debug_device_ids_list); + + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentRequestErrorInternal, + exception_message.c_str()); + } else { + has_requested_consent_info_update_ = true; + } + env->DeleteLocalRef(debug_device_ids_list); + + return MakeFuture(futures(), handle); +} + +ConsentStatus ConsentInfoInternalAndroid::GetConsentStatus() { + if (!valid()) { + return kConsentStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jint result = env->CallIntMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kGetConsentStatus)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return kConsentStatusUnknown; + } + return CppConsentStatusFromAndroid(result); +} + +ConsentFormStatus ConsentInfoInternalAndroid::GetConsentFormStatus() { + if (!valid() || !has_requested_consent_info_update_) { + return kConsentFormStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jboolean is_available = env->CallBooleanMethod( + helper_, consent_info_helper::GetMethodId( + consent_info_helper::kIsConsentFormAvailable)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return kConsentFormStatusUnknown; + } + return (is_available == JNI_FALSE) ? kConsentFormStatusUnavailable + : kConsentFormStatusAvailable; +} + +Future ConsentInfoInternalAndroid::LoadConsentForm() { + if (LoadConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kLoadConsentForm), + future_handle); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalAndroid::ShowConsentForm(FormParent parent) { + if (ShowConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); + JNIEnv* env = GetJNIEnv(); + + jlong future_handle = static_cast(handle.get().id()); + jboolean success = env->CallBooleanMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kShowConsentForm), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } else if (success == JNI_FALSE) { + CompleteFuture( + handle, kConsentFormErrorUnavailable, + "The consent form is unavailable. Please call LoadConsentForm and " + "ensure it completes successfully before calling ShowConsentForm."); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalAndroid::LoadAndShowConsentFormIfRequired( + FormParent parent) { + if (LoadAndShowConsentFormIfRequiredLastResult().status() == + kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); + + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod( + helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kLoadAndShowConsentFormIfRequired), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + + return MakeFuture(futures(), handle); +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalAndroid::GetPrivacyOptionsRequirementStatus() { + if (!valid()) { + return kPrivacyOptionsRequirementStatusUnknown; + } + JNIEnv* env = GetJNIEnv(); + jint result = env->CallIntMethod( + helper_, consent_info_helper::GetMethodId( + consent_info_helper::kGetPrivacyOptionsRequirementStatus)); + return CppPrivacyOptionsRequirementStatusFromAndroid(result); +} + +Future ConsentInfoInternalAndroid::ShowPrivacyOptionsForm( + FormParent parent) { + if (ShowPrivacyOptionsFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); + + JNIEnv* env = GetJNIEnv(); + jlong future_handle = static_cast(handle.get().id()); + + env->CallVoidMethod(helper_, + consent_info_helper::GetMethodId( + consent_info_helper::kShowPrivacyOptionsForm), + future_handle, parent); + if (env->ExceptionCheck()) { + std::string exception_message = util::GetAndClearExceptionMessage(env); + CompleteFuture(handle, kConsentFormErrorInternal, + exception_message.c_str()); + } + + return MakeFuture(futures(), handle); +} + +bool ConsentInfoInternalAndroid::CanRequestAds() { + JNIEnv* env = GetJNIEnv(); + jboolean can_request = env->CallBooleanMethod( + helper_, + consent_info_helper::GetMethodId(consent_info_helper::kCanRequestAds)); + if (env->ExceptionCheck()) { + util::CheckAndClearJniExceptions(env); + return false; + } + return (can_request == JNI_FALSE) ? false : true; +} + +void ConsentInfoInternalAndroid::Reset() { + JNIEnv* env = GetJNIEnv(); + env->CallVoidMethod( + helper_, consent_info_helper::GetMethodId(consent_info_helper::kReset)); + util::CheckAndClearJniExceptions(env); +} + +JNIEnv* ConsentInfoInternalAndroid::GetJNIEnv() { + return firebase::util::GetThreadsafeJNIEnv(java_vm_); +} +jobject ConsentInfoInternalAndroid::activity() { return activity_; } + +void ConsentInfoInternalAndroid::CompleteFutureFromJniCallback( + JNIEnv* env, jint future_fn, FutureHandleId handle_id, int java_error_code, + const char* error_message) { + if (!futures()->ValidFuture(handle_id)) { + // This future is no longer valid, so no need to complete it. + return; + } + if (future_fn < 0 || future_fn >= enums().function_count) { + // Called with an invalid function ID, ignore this callback. + return; + } + FutureHandle raw_handle(handle_id); + SafeFutureHandle handle(raw_handle); + if (future_fn == enums().function_request_consent_info_update) { + // RequestConsentInfoUpdate uses the ConsentRequestError enum. + ConsentRequestError error_code = CppConsentRequestErrorFromAndroidFormError( + java_error_code, error_message); + CompleteFuture(handle, error_code, error_message); + } else { + // All other methods use the ConsentFormError enum. + ConsentFormError error_code = + CppConsentFormErrorFromAndroidFormError(java_error_code, error_message); + CompleteFuture(handle, error_code, error_message); + } +} + +} // namespace internal +} // namespace ump +} // namespace firebase diff --git a/ump/src/android/consent_info_internal_android.h b/ump/src/android/consent_info_internal_android.h new file mode 100644 index 0000000000..f5ab3b8916 --- /dev/null +++ b/ump/src/android/consent_info_internal_android.h @@ -0,0 +1,155 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_ANDROID_CONSENT_INFO_INTERNAL_ANDROID_H_ +#define FIREBASE_UMP_SRC_ANDROID_CONSENT_INFO_INTERNAL_ANDROID_H_ + +#include + +#include "app/src/util_android.h" +#include "firebase/internal/mutex.h" +#include "ump/src/common/consent_info_internal.h" + +namespace firebase { +namespace ump { + +// clang-format off +#define UMP_INITIALIZATION_HELPER_METHODS(X) \ + X(InitializeUmp, "initializeUmp", "(Landroid/content/Context;)V", \ + util::kMethodTypeStatic) +// clang-format on + +METHOD_LOOKUP_DECLARATION(ump_initialization_helper, + UMP_INITIALIZATION_HELPER_METHODS); + +// Needed when UMP is initialized without Firebase. +JNIEnv* GetJNI(); + +// Retrieves the activity used to initialize UMP. +jobject GetActivity(); + +// Register the native callbacks needed by the Futures. +bool RegisterNatives(); + +// Release classes registered by this module. +void ReleaseClasses(JNIEnv* env); + +namespace internal { +extern ::firebase::Mutex g_cached_ump_embedded_files_mutex; +extern std::vector<::firebase::internal::EmbeddedFile>* + g_cached_ump_embedded_files; + +class ConsentInfoInternalAndroid : public ConsentInfoInternal { + public: + ConsentInfoInternalAndroid(JNIEnv* env, jobject activity); + ~ConsentInfoInternalAndroid() override; + + ConsentStatus GetConsentStatus() override; + ConsentFormStatus GetConsentFormStatus() override; + + Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) override; + Future LoadConsentForm() override; + Future ShowConsentForm(FormParent parent) override; + + Future LoadAndShowConsentFormIfRequired(FormParent parent) override; + + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus() override; + Future ShowPrivacyOptionsForm(FormParent parent) override; + + bool CanRequestAds() override; + + void Reset() override; + + bool valid() { return (helper_ != nullptr); } + + JNIEnv* GetJNIEnv(); + jobject activity(); + + private: + struct EnumCache { + jint consentstatus_unknown; + jint consentstatus_required; + jint consentstatus_not_required; + jint consentstatus_obtained; + + jint formerror_success; + jint formerror_internal; + jint formerror_network; + jint formerror_invalid_operation; + jint formerror_timeout; + + jint debug_geography_disabled; + jint debug_geography_eea; + jint debug_geography_not_eea; + + jint privacy_options_requirement_unknown; + jint privacy_options_requirement_required; + jint privacy_options_requirement_not_required; + + jint function_request_consent_info_update; + jint function_load_consent_form; + jint function_show_consent_form; + jint function_load_and_show_consent_form_if_required; + jint function_show_privacy_options_form; + jint function_count; + }; + + // JNI native method callback for ConsentInfoHelper.completeFuture. + // Calls CompleteFutureFromJniCallback() below. + static void JNI_ConsentInfoHelper_completeFuture( + JNIEnv* env, jclass clazz, jint future_fn, + jlong consent_info_internal_ptr, jlong future_handle, jint error_code, + jobject error_message_obj); + + // Complete the given Future when called from JNI. + void CompleteFutureFromJniCallback(JNIEnv* env, jint future_fn, + FutureHandleId handle_id, int error_code, + const char* error_message); + + // Cache Java enum field values in the struct below. + void CacheEnumValues(JNIEnv* env); + + // Enum conversion methods. + ConsentStatus CppConsentStatusFromAndroid(jint status); + PrivacyOptionsRequirementStatus CppPrivacyOptionsRequirementStatusFromAndroid( + jint status); + jint AndroidDebugGeographyFromCppDebugGeography(ConsentDebugGeography geo); + ConsentRequestError CppConsentRequestErrorFromAndroidFormError( + jint error, const char* message = nullptr); + ConsentFormError CppConsentFormErrorFromAndroidFormError( + jint error, const char* message = nullptr); + + const EnumCache& enums() { return enums_; } + + static ConsentInfoInternalAndroid* s_instance; + static firebase::Mutex s_instance_mutex; + + EnumCache enums_; + + JavaVM* java_vm_; + jobject activity_; + jobject helper_; + + // Needed for GetConsentFormStatus to return Unknown. + bool has_requested_consent_info_update_; +}; + +} // namespace internal +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_UMP_SRC_ANDROID_CONSENT_INFO_INTERNAL_ANDROID_H_ diff --git a/ump/src/common/consent_info.cc b/ump/src/common/consent_info.cc new file mode 100644 index 0000000000..a93872484c --- /dev/null +++ b/ump/src/common/consent_info.cc @@ -0,0 +1,182 @@ +/* + * 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. + */ + +#include "firebase/ump/consent_info.h" + +#include "app/src/assert.h" +#include "firebase/app.h" +#include "firebase/internal/platform.h" +#include "firebase/ump.h" +#include "ump//src/common/consent_info_internal.h" + +namespace firebase { +namespace ump { + +ConsentInfo* ConsentInfo::s_instance_ = nullptr; + +ConsentInfo* ConsentInfo::GetInstance(const ::firebase::App& app, + ::firebase::InitResult* init_result_out) { + if (s_instance_) { + if (init_result_out) *init_result_out = kInitResultSuccess; + return s_instance_; + } +#if FIREBASE_PLATFORM_ANDROID + return GetInstance(app.GetJNIEnv(), app.activity(), init_result_out); +#else // !FIREBASE_PLATFORM_ANDROID + return GetInstance(init_result_out); +#endif // FIREBASE_PLATFORM_ANDROID +} + +#if FIREBASE_PLATFORM_ANDROID +ConsentInfo* ConsentInfo::GetInstance() { return s_instance_; } + +ConsentInfo* ConsentInfo::GetInstance(JNIEnv* jni_env, jobject activity, + ::firebase::InitResult* init_result_out) { +#else // !FIREBASE_PLATFORM_ANDROID +ConsentInfo* ConsentInfo::GetInstance(::firebase::InitResult* init_result_out) { +#endif + if (s_instance_) { + if (init_result_out) *init_result_out = kInitResultSuccess; + return s_instance_; + } + + ConsentInfo* consent_info = new ConsentInfo(); +#if FIREBASE_PLATFORM_ANDROID + InitResult result = consent_info->Initialize(jni_env, activity); +#else + InitResult result = consent_info->Initialize(); +#endif + if (init_result_out) *init_result_out = result; + if (result != kInitResultSuccess) { + delete consent_info; + return nullptr; + } + return consent_info; +} + +ConsentInfo::ConsentInfo() { + internal_ = nullptr; +#if FIREBASE_PLATFORM_ANDROID + java_vm_ = nullptr; +#endif + s_instance_ = this; +} + +ConsentInfo::~ConsentInfo() { + if (internal_) { + delete internal_; + internal_ = nullptr; + } + s_instance_ = nullptr; +} + +#if FIREBASE_PLATFORM_ANDROID +InitResult ConsentInfo::Initialize(JNIEnv* jni_env, jobject activity) { + FIREBASE_ASSERT(!internal_); + internal_ = internal::ConsentInfoInternal::CreateInstance(jni_env, activity); + return internal_ ? kInitResultSuccess : kInitResultFailedMissingDependency; +} +#else +InitResult ConsentInfo::Initialize() { + FIREBASE_ASSERT(!internal_); + internal_ = internal::ConsentInfoInternal::CreateInstance(); + return kInitResultSuccess; +} +#endif + +// Below this, everything is a passthrough to ConsentInfoInternal. If there is +// no internal_ pointer (e.g. it's been cleaned up), return default values and +// invalid futures. + +ConsentStatus ConsentInfo::GetConsentStatus() { + if (!internal_) return kConsentStatusUnknown; + return internal_->GetConsentStatus(); +} + +ConsentFormStatus ConsentInfo::GetConsentFormStatus() { + if (!internal_) return kConsentFormStatusUnknown; + return internal_->GetConsentFormStatus(); +} + +Future ConsentInfo::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + if (!internal_) return Future(); + return internal_->RequestConsentInfoUpdate(params); +} + +Future ConsentInfo::RequestConsentInfoUpdateLastResult() { + if (!internal_) return Future(); + return internal_->RequestConsentInfoUpdateLastResult(); +} + +Future ConsentInfo::LoadConsentForm() { + if (!internal_) return Future(); + return internal_->LoadConsentForm(); +} + +Future ConsentInfo::LoadConsentFormLastResult() { + if (!internal_) return Future(); + return internal_->LoadConsentFormLastResult(); +} + +Future ConsentInfo::ShowConsentForm(FormParent parent) { + if (!internal_) return Future(); + return internal_->ShowConsentForm(parent); +} + +Future ConsentInfo::ShowConsentFormLastResult() { + if (!internal_) return Future(); + return internal_->ShowConsentFormLastResult(); +} + +Future ConsentInfo::LoadAndShowConsentFormIfRequired(FormParent parent) { + if (!internal_) return Future(); + return internal_->LoadAndShowConsentFormIfRequired(parent); +} + +Future ConsentInfo::LoadAndShowConsentFormIfRequiredLastResult() { + if (!internal_) return Future(); + return internal_->LoadAndShowConsentFormIfRequiredLastResult(); +} + +PrivacyOptionsRequirementStatus +ConsentInfo::GetPrivacyOptionsRequirementStatus() { + if (!internal_) return kPrivacyOptionsRequirementStatusUnknown; + return internal_->GetPrivacyOptionsRequirementStatus(); +} + +Future ConsentInfo::ShowPrivacyOptionsForm(FormParent parent) { + if (!internal_) return Future(); + return internal_->ShowPrivacyOptionsForm(parent); +} + +Future ConsentInfo::ShowPrivacyOptionsFormLastResult() { + if (!internal_) return Future(); + return internal_->ShowPrivacyOptionsFormLastResult(); +} + +bool ConsentInfo::CanRequestAds() { + if (!internal_) return false; + return internal_->CanRequestAds(); +} + +void ConsentInfo::Reset() { + if (!internal_) return; + internal_->Reset(); +} + +} // namespace ump +} // namespace firebase diff --git a/ump/src/common/consent_info_internal.cc b/ump/src/common/consent_info_internal.cc new file mode 100644 index 0000000000..ca6f4677f6 --- /dev/null +++ b/ump/src/common/consent_info_internal.cc @@ -0,0 +1,88 @@ +/* + * 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. + */ + +#include "ump/src/common/consent_info_internal.h" + +#include "app/src/include/firebase/internal/platform.h" + +namespace firebase { +namespace ump { +namespace internal { + +ConsentInfoInternal::ConsentInfoInternal() : futures_(kConsentInfoFnCount) {} + +ConsentInfoInternal::~ConsentInfoInternal() {} + +const char* ConsentInfoInternal::GetConsentRequestErrorMessage( + ConsentRequestError error_code) { + switch (error_code) { + case kConsentRequestSuccess: + return "Success"; + case kConsentRequestErrorInvalidAppId: +#if FIREBASE_PLATFORM_ANDROID + return "Missing or invalid com.google.android.gms.ads.APPLICATION_ID in " + "AndroidManifest.xml"; +#elif FIREBASE_PLATFORM_IOS + return "Missing or invalid GADApplicationidentifier in Info.plist"; +#else + return "Missing or invalid App ID"; +#endif + case kConsentRequestErrorNetwork: + return "Network error"; + case kConsentRequestErrorInternal: + return "Internal error"; + case kConsentRequestErrorMisconfiguration: + return "A misconfiguration exists in the UI"; + case kConsentRequestErrorUnknown: + return "Unknown error"; + case kConsentRequestErrorInvalidOperation: + return "Invalid operation"; + case kConsentRequestErrorOperationInProgress: + return "Operation already in progress. Please wait for it to finish by " + "checking RequestConsentInfoUpdateLastResult()."; + default: + return "Bad error code"; + } +} + +const char* ConsentInfoInternal::GetConsentFormErrorMessage( + ConsentFormError error_code) { + switch (error_code) { + case kConsentFormSuccess: + return "Success"; + case kConsentFormErrorTimeout: + return "Timed out"; + case kConsentFormErrorUnavailable: + return "The form is unavailable."; + case kConsentFormErrorInternal: + return "Internal error"; + case kConsentFormErrorUnknown: + return "Unknown error"; + case kConsentFormErrorAlreadyUsed: + return "The form was already used"; + case kConsentFormErrorInvalidOperation: + return "Invalid operation"; + case kConsentFormErrorOperationInProgress: + return "Operation already in progress. Please wait for it to finish by " + "checking LoadFormLastResult() or ShowFormLastResult()."; + default: + return "Bad error code"; + } +} + +} // namespace internal +} // namespace ump +} // namespace firebase diff --git a/ump/src/common/consent_info_internal.h b/ump/src/common/consent_info_internal.h new file mode 100644 index 0000000000..0578478653 --- /dev/null +++ b/ump/src/common/consent_info_internal.h @@ -0,0 +1,140 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_COMMON_CONSENT_INFO_INTERNAL_H_ +#define FIREBASE_UMP_SRC_COMMON_CONSENT_INFO_INTERNAL_H_ + +#include "app/src/cleanup_notifier.h" +#include "app/src/reference_counted_future_impl.h" +#include "firebase/future.h" +#include "firebase/internal/platform.h" +#include "firebase/ump.h" +#include "firebase/ump/types.h" + +#if FIREBASE_PLATFORM_ANDROID +#include +#endif + +namespace firebase { +namespace ump { +namespace internal { + +// Constants representing each ConsentInfo function that returns a Future. +enum ConsentInfoFn { + kConsentInfoFnRequestConsentInfoUpdate, + kConsentInfoFnLoadConsentForm, + kConsentInfoFnShowConsentForm, + kConsentInfoFnLoadAndShowConsentFormIfRequired, + kConsentInfoFnShowPrivacyOptionsForm, + kConsentInfoFnCount +}; + +class ConsentInfoInternal { + public: + virtual ~ConsentInfoInternal(); + + // Implemented in platform-specific code to instantiate a + // platform-specific subclass. +#if FIREBASE_PLATFORM_ANDROID + static ConsentInfoInternal* CreateInstance(JNIEnv* jni_env, jobject activity); +#else + static ConsentInfoInternal* CreateInstance(); +#endif + + virtual ConsentStatus GetConsentStatus() = 0; + virtual ConsentFormStatus GetConsentFormStatus() = 0; + + virtual Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) = 0; + Future RequestConsentInfoUpdateLastResult() { + return static_cast&>( + futures()->LastResult(kConsentInfoFnRequestConsentInfoUpdate)); + } + virtual Future LoadConsentForm() = 0; + + Future LoadConsentFormLastResult() { + return static_cast&>( + futures()->LastResult(kConsentInfoFnLoadConsentForm)); + } + + virtual Future ShowConsentForm(FormParent parent) = 0; + + Future ShowConsentFormLastResult() { + return static_cast&>( + futures()->LastResult(kConsentInfoFnShowConsentForm)); + } + + virtual Future LoadAndShowConsentFormIfRequired(FormParent parent) = 0; + + Future LoadAndShowConsentFormIfRequiredLastResult() { + return static_cast&>( + futures()->LastResult(kConsentInfoFnLoadAndShowConsentFormIfRequired)); + } + + virtual PrivacyOptionsRequirementStatus + GetPrivacyOptionsRequirementStatus() = 0; + + virtual Future ShowPrivacyOptionsForm(FormParent parent) = 0; + + Future ShowPrivacyOptionsFormLastResult() { + return static_cast&>( + futures()->LastResult(kConsentInfoFnShowPrivacyOptionsForm)); + } + + virtual bool CanRequestAds() = 0; + + virtual void Reset() = 0; + + protected: + ConsentInfoInternal(); + + static const char* GetConsentRequestErrorMessage( + ConsentRequestError error_code); + + static const char* GetConsentFormErrorMessage(ConsentFormError error_code); + + SafeFutureHandle CreateFuture() { return futures()->SafeAlloc(); } + SafeFutureHandle CreateFuture(ConsentInfoFn fn_idx) { + return futures()->SafeAlloc(fn_idx); + } + + // Complete a Future with the given error code. + void CompleteFuture(SafeFutureHandle handle, ConsentRequestError error, + const char* message = nullptr) { + return futures()->Complete( + handle, error, + message ? message : GetConsentRequestErrorMessage(error)); + } + // Complete the future with the given error code. + void CompleteFuture(SafeFutureHandle handle, ConsentFormError error, + const char* message = nullptr) { + return futures()->Complete( + handle, error, message ? message : GetConsentFormErrorMessage(error)); + } + + ReferenceCountedFutureImpl* futures() { return &futures_; } + CleanupNotifier* cleanup() { return &cleanup_; } + + private: + ReferenceCountedFutureImpl futures_; + CleanupNotifier cleanup_; +}; + +} // namespace internal +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_UMP_SRC_COMMON_CONSENT_INFO_INTERNAL_H_ diff --git a/ump/src/include/firebase/ump.h b/ump/src/include/firebase/ump.h new file mode 100644 index 0000000000..bd33f1f238 --- /dev/null +++ b/ump/src/include/firebase/ump.h @@ -0,0 +1,23 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_H_ +#define FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_H_ + +#include "firebase/ump/consent_info.h" +#include "firebase/ump/types.h" + +#endif // FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_H_ diff --git a/ump/src/include/firebase/ump/consent_info.h b/ump/src/include/firebase/ump/consent_info.h new file mode 100644 index 0000000000..10d670c821 --- /dev/null +++ b/ump/src/include/firebase/ump/consent_info.h @@ -0,0 +1,247 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_CONSENT_INFO_H_ +#define FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_CONSENT_INFO_H_ + +#include "firebase/app.h" +#include "firebase/future.h" +#include "firebase/internal/platform.h" +#include "firebase/ump/types.h" + +#if FIREBASE_PLATFORM_ANDROID +#include +#endif // FIREBASE_PLATFORM_ANDROID + +namespace firebase { +/// @brief API for User Messaging Platform. +/// +/// The User Messaging Platform (UMP) SDK is Google’s option to handle user +/// privacy and consent in mobile apps. +namespace ump { + +namespace internal { +// Forward declaration for platform-specific data, implemented in each library. +class ConsentInfoInternal; +} // namespace internal + +/// @brief Consent Information class for the User Messaging Platform SDK. +/// +/// The User Messaging Platform (UMP) SDK is Google’s option to handle user +/// privacy and consent in mobile apps. +/// +/// This class contains all of the methods necessary for obtaining +/// consent from the user. +class ConsentInfo { + public: + /// Shut down the User Messaging Platform Consent SDK. + ~ConsentInfo(); + + /// Initializes the User Messaging Platform Consent SDK. + /// + /// @param[in] app Any Firebase App instance. + /// + /// @param[out] init_result_out Optional: If provided, write the basic init + /// result here. kInitResultSuccess if initialization succeeded, or + /// kInitResultFailedMissingDependency on Android if there are Android + /// dependencies missing. + /// + /// @return A pointer to the ConsentInfo instance if UMP was successfully + /// initialized, nullptr otherwise. Each call to GetInstance() will return the + /// same pointer; when you are finished using the SDK, you can delete the + /// pointer and the UMP SDK will shut down. + static ConsentInfo* GetInstance(const ::firebase::App& app, + InitResult* init_result_out = nullptr); + +#if FIREBASE_PLATFORM_ANDROID || defined(DOXYGEN) + /// Initializes the User Messaging Platform Consent SDK without Firebase for + /// Android. + /// + /// The arguments to GetInstance() are platform-specific so the caller must + /// do something like this: + /// @code + /// #if defined(__ANDROID__) + /// consent_info = firebase::ump::ConsentInfo::GetInstance(jni_env, + /// activity); + /// #else + /// consent_info = firebase::ump::GetInstance(); + /// #endif + /// @endcode + /// + /// @param[in] jni_env JNIEnv pointer. + /// @param[in] activity Activity used to start the application. + /// @param[out] init_result_out Optional: If provided, write the basic init + /// result here. kInitResultSuccess if initialization succeeded, or + /// kInitResultFailedMissingDependency on Android if there are Android + /// dependencies missing. + /// + /// @return A pointer to the ConsentInfo instance if UMP was successfully + /// initialized, nullptr otherwise. Each call to GetInstance() will return the + /// same pointer; when you are finished using the SDK, you can delete the + /// pointer and the UMP SDK will shut down. + static ConsentInfo* GetInstance(JNIEnv* jni_env, jobject activity, + InitResult* init_result_out = nullptr); + +#if !defined(DOXYGEN) + // On Android, this convenience function exists so you can easily get the + // existing ConsentInfo instance after it's first initialized. Returns nullptr + // if no instance has been created yet; make sure you have called + // GetInstance(JNIEnv*, jobject) first. + static ConsentInfo* GetInstance(); +#endif // defined(DOXYGEN) +#endif // FIREBASE_PLATFORM_ANDROID || defined(DOXYGEN) + +#if !FIREBASE_PLATFORM_ANDROID || defined(DOXYGEN) + /// Initializes User Messaging Platform for iOS without Firebase. + /// + /// @param[out] init_result_out Optional: If provided, write the basic init + /// result here. kInitResultSuccess if initialization succeeded, or + /// kInitResultFailedMissingDependency if a dependency is missing. On iOS, + /// this will always return kInitResultSuccess, as missing dependencies would + /// have caused a linker error at build time. + /// + /// @return A pointer to the ConsentInfo instance. Each call to GetInstance() + /// will return the same pointer; when you are finished using the SDK, you can + /// delete the pointer, and the UMP SDK will shut down. + /// + /// @note Once any overload of ConsentInfo::GetInstance has been called, you + /// can use this method to obtain the same instance again. + static ConsentInfo* GetInstance(InitResult* init_result_out = nullptr); +#endif // !defined(__ANDROID__) || defined(DOXYGEN) + + /// The user’s consent status. This value defaults to kConsentStatusUnknown + /// until RequestConsentInfoUpdate() is called, and defaults to the previous + /// session’s value until RequestConsentInfoUpdate() completes. + ConsentStatus GetConsentStatus(); + + /// Requests consent information update. Must be called in every app session + /// before checking the user’s consent status or loading a consent form. After + /// calling this method, GetConsentStatus() and CanRequestAds() will be + /// updated immediately to hold the consent state from the previous app + /// session, if one exists. GetConsentStatus() and CanRequestAds() may be + /// updated again immediately before the returned future is completed. + Future RequestConsentInfoUpdate(const ConsentRequestParameters& params); + + /// Get the Future from the most recent call to RequestConsentInfoUpdate(). + Future RequestConsentInfoUpdateLastResult(); + + /// Consent form status. This value defaults to kConsentFormStatusUnknown and + /// requires a call to RequestConsentInfoUpdate() to update. + ConsentFormStatus GetConsentFormStatus(); + + /// Loads a consent form. Returns an error if the consent form is unavailable + /// or cannot be loaded. + Future LoadConsentForm(); + + /// Get the Future from the most recent call to LoadConsentForm(). + Future LoadConsentFormLastResult(); + + /// Presents the full screen consent form using the given FormParent, which is + /// defined as an Activity on Android and a UIViewController on iOS. The form + /// will be dismissed and the Future will be completed after the user selects + /// an option. + /// + /// GetConsentStatus() and CanRequestAds() are updated when the returned + /// Future is completed. + /// + /// @param[in] parent A FormParent, which is an Activity object on Android and + /// a UIViewController object on iOS. + /// + /// @note You must call LoadConsentForm() and wait for it to complete before + /// calling this method. + Future ShowConsentForm(FormParent parent); + + /// Get the Future from the most recent call to ShowConsentForm(). + Future ShowConsentFormLastResult(); + + /// Loads a consent form and immediately presents it using the given + /// FormParent, if ConsentStatus is kConsentStatusRequired. The FormParent is + /// defined as an Activity on Android and a UIViewController on iOS. The + /// Future will be completed successfully after the user selects an option + /// (and the form is dismissed), or if the form is not required. The Future + /// will be completed with an error if the form fails to load or show. + /// + /// GetConsentStatus() and CanRequestAds() will be updated prior to the Future + /// being completed. + /// + /// @param[in] parent A FormParent, which is an Activity object on Android and + /// a UIViewController object on iOS. + Future LoadAndShowConsentFormIfRequired(FormParent parent); + + /// Get the Future from the most recent call to + /// LoadAndShowConsentFormIfRequired(). + Future LoadAndShowConsentFormIfRequiredLastResult(); + + /// Check whether the privacy options form needs to be displayed. + /// This is updated by RequestConsentInfoUpdate(). + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus(); + + /// If GetPrivacyOptionsRequirementStatus() is + /// kPrivacyOptionsRequirementStatusRequired, presents a privacy options form + /// from the provided FormParent, which is defined as an Activity on Android + /// and a UIViewController on iOS. + /// + /// This method should only be called in response to a user input to request a + /// privacy options form to be shown. + /// + /// The future completes when the user selects an option and dismisses the + /// form or is completed immediately with an error code if no form is + /// presented. The privacy options form is preloaded by the SDK automatically + /// when a form becomes available. If no form has been preloaded, the SDK will + /// try to load one asynchronously. + /// + /// @param[in] parent A FormParent, which is an Activity object on Android and + /// a UIViewController object on iOS. + Future ShowPrivacyOptionsForm(FormParent parent); + + /// Get the Future from the most recent call to ShowPrivacyOptionsForm(). + Future ShowPrivacyOptionsFormLastResult(); + + /// Indicates whether the app has completed the necessary steps for gathering + /// updated user consent. Returns true if RequestConsentInfoUpdate() has been + /// called and GetConsentStatus returns either kConsentStatusNotRequired or + /// kConsentStatusObtained. + bool CanRequestAds(); + + /// Clears all consent state from persistent storage. This can be used in + /// development to simulate a new installation. + void Reset(); + + private: + ConsentInfo(); +#if FIREBASE_PLATFORM_ANDROID + InitResult Initialize(JNIEnv* jni_env, jobject activity); +#else + InitResult Initialize(); +#endif // FIREBASE_PLATFORM_ANDROID + void Terminate(); + + static ConsentInfo* s_instance_; + +#if FIREBASE_PLATFORM_ANDROID + JavaVM* java_vm() { return java_vm_; } + JavaVM* java_vm_; +#endif + + // An internal, platform-specific implementation object that this class uses + // to interact with the User Messaging Platform SDKs for iOS and Android. + internal::ConsentInfoInternal* internal_; +}; + +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_CONSENT_INFO_H_ diff --git a/ump/src/include/firebase/ump/types.h b/ump/src/include/firebase/ump/types.h new file mode 100644 index 0000000000..d376bff85d --- /dev/null +++ b/ump/src/include/firebase/ump/types.h @@ -0,0 +1,177 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_TYPES_H_ +#define FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_TYPES_H_ + +#include +#include +#include + +#include "firebase/internal/platform.h" + +#if FIREBASE_PLATFORM_ANDROID +#include +#elif FIREBASE_PLATFORM_IOS || FIREBASE_PLATFORM_TVOS +extern "C" { +#include +} // extern "C" +#endif // FIREBASE_PLATFORM_ANDROID, FIREBASE_PLATFORM_IOS, + // FIREBASE_PLATFORM_TVOS + +namespace firebase { +namespace ump { + +/// Debug values for testing geography. +enum ConsentDebugGeography { + /// Disable geography debugging. + kConsentDebugGeographyDisabled = 0, + /// Geography appears as in EEA (European Economic Area) for debug devices. + kConsentDebugGeographyEEA, + /// Geography appears as not in EEA for debug devices. + kConsentDebugGeographyNonEEA +}; + +/// Debug settings for `ConsentInfo::RequestConsentInfoUpdate()`. These let you +/// force a specific geographic location. Be sure to include debug device IDs to +/// enable this on hardware. Debug features are always enabled for simulators. +struct ConsentDebugSettings { + /// Create a default debug setting, with debugging disabled. + ConsentDebugSettings() : debug_geography(kConsentDebugGeographyDisabled) {} + + /// The geographical location, for debugging. + ConsentDebugGeography debug_geography; + /// A list of all device IDs that are allowed to use debug settings. You can + /// obtain this from the device log after running with debug settings enabled. + std::vector debug_device_ids; +}; + +/// Parameters for the `ConsentInfo::RequestConsentInfoUpdate()` operation. +struct ConsentRequestParameters { + ConsentRequestParameters() : tag_for_under_age_of_consent(false) {} + + /// Debug settings for the consent request. + ConsentDebugSettings debug_settings; + + /// Whether the user is under the age of consent. + bool tag_for_under_age_of_consent; +}; + +/// This is a platform specific datatype that is required to show a consent form +/// on screen. +/// +/// The following defines the datatype on each platform: +///
      +///
    • Android: A `jobject` which references an Android Activity.
    • +///
    • iOS: An `id` which references an iOS UIViewController.
    • +///
    +#if FIREBASE_PLATFORM_ANDROID +/// An Android Activity from Java. +typedef jobject FormParent; +#elif FIREBASE_PLATFORM_IOS || FIREBASE_PLATFORM_TVOS +/// A pointer to an iOS UIViewController. +typedef id FormParent; +#else +/// A void pointer for stub classes. +typedef void* FormParent; +#endif // FIREBASE_PLATFORM_ANDROID, FIREBASE_PLATFORM_IOS, + // FIREBASE_PLATFORM_TVOS + +/// Consent status values. +enum ConsentStatus { + /// Unknown status, e.g. prior to calling Request, or if the request fails. + kConsentStatusUnknown = 0, + /// Consent is required, but not obtained + kConsentStatusRequired, + /// Consent is not required + kConsentStatusNotRequired, + /// Consent was required, and has been obtained + kConsentStatusObtained +}; + +/// Errors that can occur during a RequestConsentInfoUpdate operation. +enum ConsentRequestError { + /// The operation succeeded. + kConsentRequestSuccess = 0, + /// Invalid GMA App ID specified in AndroidManifest.xml or Info.plist. + kConsentRequestErrorInvalidAppId, + /// A network error occurred. + kConsentRequestErrorNetwork, + /// An internal error occurred. + kConsentRequestErrorInternal, + /// A misconfiguration exists in the UI. + kConsentRequestErrorMisconfiguration, + /// An unknown error occurred. + kConsentRequestErrorUnknown, + /// An invalid operation occurred. Try again. + kConsentRequestErrorInvalidOperation, + /// The operation is already in progress. Use + /// `ConsentInfo::RequestConsentInfoUpdateLastResult()` + /// to get the status. + kConsentRequestErrorOperationInProgress +}; + +/// Status of the consent form, whether it is available to show or not. +enum ConsentFormStatus { + /// Status is unknown. Call `ConsentInfo::RequestConsentInfoUpdate()` to + /// update this. + kConsentFormStatusUnknown = 0, + /// The consent form is unavailable. Call `ConsentInfo::LoadConsentForm()` to + /// load it. + kConsentFormStatusUnavailable, + /// The consent form is available. Call `ConsentInfo::ShowConsentForm()` to + /// display it. + kConsentFormStatusAvailable, +}; + +/// Errors when loading or showing the consent form. +enum ConsentFormError { + /// The operation succeeded. + kConsentFormSuccess = 0, + /// The load request timed out. Try again. + kConsentFormErrorTimeout, + /// An internal error occurred. + kConsentFormErrorInternal, + /// An unknown error occurred. + kConsentFormErrorUnknown, + /// The form is unavailable. + kConsentFormErrorUnavailable, + /// This form was already used. + kConsentFormErrorAlreadyUsed, + /// An invalid operation occurred. Try again. + kConsentFormErrorInvalidOperation, + /// The operation is already in progress. Call + /// `ConsentInfo::LoadConsentFormLastResult()` or + /// `ConsentInfo::ShowConsentFormLastResult()` to get the status. + kConsentFormErrorOperationInProgress +}; + +/// Whether the privacy options need to be displayed. +enum PrivacyOptionsRequirementStatus { + /// Privacy options requirement status is unknown. Call + /// `ConsentInfo::RequestConsentInfoUpdate()` to update. + kPrivacyOptionsRequirementStatusUnknown = 0, + /// Privacy options are not required to be shown. + kPrivacyOptionsRequirementStatusNotRequired, + /// Privacy options must be shown. Call + /// `ConsentInfo::ShowPrivacyOptionsForm()` to fulfil this requirement. + kPrivacyOptionsRequirementStatusRequired +}; + +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_UMP_SRC_INCLUDE_FIREBASE_UMP_TYPES_H_ diff --git a/ump/src/ios/.clang-format b/ump/src/ios/.clang-format new file mode 100644 index 0000000000..9d159247d5 --- /dev/null +++ b/ump/src/ios/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: false diff --git a/ump/src/ios/consent_info_internal_ios.h b/ump/src/ios/consent_info_internal_ios.h new file mode 100644 index 0000000000..f1db540258 --- /dev/null +++ b/ump/src/ios/consent_info_internal_ios.h @@ -0,0 +1,67 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_IOS_CONSENT_INFO_INTERNAL_IOS_H_ +#define FIREBASE_UMP_SRC_IOS_CONSENT_INFO_INTERNAL_IOS_H_ + +#include + +#include "firebase/internal/mutex.h" +#include "ump/src/common/consent_info_internal.h" + +namespace firebase { +namespace ump { +namespace internal { + +class ConsentInfoInternalIos : public ConsentInfoInternal { + public: + ConsentInfoInternalIos(); + ~ConsentInfoInternalIos() override; + + ConsentStatus GetConsentStatus() override; + Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) override; + + ConsentFormStatus GetConsentFormStatus() override; + Future LoadConsentForm() override; + Future ShowConsentForm(FormParent parent) override; + + Future LoadAndShowConsentFormIfRequired(FormParent parent) override; + + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus() override; + Future ShowPrivacyOptionsForm(FormParent parent) override; + + bool CanRequestAds() override; + + void Reset() override; + + private: + static ConsentInfoInternalIos* s_instance; + static firebase::Mutex s_instance_mutex; + static unsigned int s_instance_tag; + + void SetLoadedForm(UMPConsentForm *form) { + loaded_form_ = form; + } + + UMPConsentForm *loaded_form_; +}; + +} // namespace internal +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_UMP_SRC_IOS_CONSENT_INFO_INTERNAL_IOS_H_ diff --git a/ump/src/ios/consent_info_internal_ios.mm b/ump/src/ios/consent_info_internal_ios.mm new file mode 100644 index 0000000000..9bb15f1f1a --- /dev/null +++ b/ump/src/ios/consent_info_internal_ios.mm @@ -0,0 +1,367 @@ +/* + * 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. + */ + +#include "ump/src/ios/consent_info_internal_ios.h" + +#include "app/src/assert.h" +#include "app/src/thread.h" +#include "app/src/util_ios.h" + +namespace firebase { +namespace ump { +namespace internal { + +ConsentInfoInternalIos* ConsentInfoInternalIos::s_instance = nullptr; +firebase::Mutex ConsentInfoInternalIos::s_instance_mutex; +unsigned int ConsentInfoInternalIos::s_instance_tag = 0; + +// This explicitly implements the constructor for the outer class, +// ConsentInfoInternal. +ConsentInfoInternal* ConsentInfoInternal::CreateInstance() { return new ConsentInfoInternalIos(); } + +ConsentInfoInternalIos::ConsentInfoInternalIos() : loaded_form_(nil) { + MutexLock lock(s_instance_mutex); + FIREBASE_ASSERT(s_instance == nullptr); + s_instance = this; + // Increment this with each created instance, to ensure that any leftover + // callbacks don't run if a new instance is created. + s_instance_tag++; +} + +ConsentInfoInternalIos::~ConsentInfoInternalIos() { + MutexLock lock(s_instance_mutex); + s_instance = nullptr; +} + +static ConsentRequestError CppRequestErrorFromIosRequestError(NSInteger code) { + switch (code) { + case UMPRequestErrorCodeInternal: + return kConsentRequestErrorInternal; + case UMPRequestErrorCodeInvalidAppID: + return kConsentRequestErrorInvalidAppId; + case UMPRequestErrorCodeMisconfiguration: + return kConsentRequestErrorMisconfiguration; + case UMPRequestErrorCodeNetwork: + return kConsentRequestErrorNetwork; + default: + LogWarning("UMP: Unknown UMPRequestErrorCode returned by UMP iOS SDK: %d", (int)code); + return kConsentRequestErrorUnknown; + } +} + +static ConsentFormError CppFormErrorFromIosFormError(NSInteger code) { + switch (code) { + case UMPFormErrorCodeInternal: + return kConsentFormErrorInternal; + case UMPFormErrorCodeAlreadyUsed: + return kConsentFormErrorAlreadyUsed; + case UMPFormErrorCodeUnavailable: + return kConsentFormErrorUnavailable; + case UMPFormErrorCodeTimeout: + return kConsentFormErrorTimeout; + case UMPFormErrorCodeInvalidViewController: + return kConsentFormErrorInvalidOperation; + default: + LogWarning("UMP: Unknown UMPFormErrorCode returned by UMP iOS SDK: %d", (int)code); + return kConsentFormErrorUnknown; + } +} + +Future ConsentInfoInternalIos::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + MutexLock lock(s_instance_mutex); + if (RequestConsentInfoUpdateLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentRequestErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); + + UMPRequestParameters* ios_parameters = [[UMPRequestParameters alloc] init]; + ios_parameters.tagForUnderAgeOfConsent = params.tag_for_under_age_of_consent ? YES : NO; + UMPDebugSettings* ios_debug_settings = [[UMPDebugSettings alloc] init]; + bool has_debug_settings = false; + + switch (params.debug_settings.debug_geography) { + case kConsentDebugGeographyEEA: + ios_debug_settings.geography = UMPDebugGeographyEEA; + has_debug_settings = true; + break; + case kConsentDebugGeographyNonEEA: + ios_debug_settings.geography = UMPDebugGeographyNotEEA; + has_debug_settings = true; + break; + case kConsentDebugGeographyDisabled: + ios_debug_settings.geography = UMPDebugGeographyDisabled; + break; + } + if (params.debug_settings.debug_device_ids.size() > 0) { + ios_debug_settings.testDeviceIdentifiers = + firebase::util::StringVectorToNSMutableArray(params.debug_settings.debug_device_ids); + has_debug_settings = true; + } + if (has_debug_settings) { + ios_parameters.debugSettings = ios_debug_settings; + } + + unsigned int callback_instance_tag; + callback_instance_tag = s_instance_tag; + + util::DispatchAsyncSafeMainQueue(^{ + MutexLock lock(s_instance_mutex); + if (!s_instance || s_instance_tag != callback_instance_tag) { + // Instance changed or was invalidated, don't call the iOS method any more. + return; + } + [UMPConsentInformation.sharedInstance + requestConsentInfoUpdateWithParameters:ios_parameters + completionHandler:^(NSError* _Nullable error) { + MutexLock lock(s_instance_mutex); + if (s_instance && s_instance_tag == callback_instance_tag) { + if (!error) { + CompleteFuture(handle, kConsentRequestSuccess); + } else { + CompleteFuture(handle, + CppRequestErrorFromIosRequestError(error.code), + error.localizedDescription.UTF8String); + } + } + }]; + }); + + return MakeFuture(futures(), handle); +} + +ConsentStatus ConsentInfoInternalIos::GetConsentStatus() { + UMPConsentStatus ios_status = UMPConsentInformation.sharedInstance.consentStatus; + switch (ios_status) { + case UMPConsentStatusNotRequired: + return kConsentStatusNotRequired; + case UMPConsentStatusRequired: + return kConsentStatusRequired; + case UMPConsentStatusObtained: + return kConsentStatusObtained; + case UMPConsentStatusUnknown: + return kConsentStatusUnknown; + default: + LogWarning("UMP: Unknown UMPConsentStatus returned by UMP iOS SDK: %d", (int)ios_status); + return kConsentStatusUnknown; + } +} + +ConsentFormStatus ConsentInfoInternalIos::GetConsentFormStatus() { + UMPFormStatus ios_status = UMPConsentInformation.sharedInstance.formStatus; + switch (ios_status) { + case UMPFormStatusAvailable: + return kConsentFormStatusAvailable; + case UMPFormStatusUnavailable: + return kConsentFormStatusUnavailable; + case UMPFormStatusUnknown: + return kConsentFormStatusUnknown; + default: + LogWarning("UMP: Unknown UMPFormConsentStatus returned by UMP iOS SDK: %d", (int)ios_status); + return kConsentFormStatusUnknown; + } +} + +Future ConsentInfoInternalIos::LoadConsentForm() { + MutexLock lock(s_instance_mutex); + if (LoadConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); + loaded_form_ = nil; + + unsigned int callback_instance_tag; + callback_instance_tag = s_instance_tag; + + util::DispatchAsyncSafeMainQueue(^{ + MutexLock lock(s_instance_mutex); + if (!s_instance || s_instance_tag != callback_instance_tag) { + // Instance changed or was invalidated, don't call the iOS method any more. + return; + } + [UMPConsentForm + loadWithCompletionHandler:^(UMPConsentForm* _Nullable form, NSError* _Nullable error) { + MutexLock lock(s_instance_mutex); + if (s_instance && s_instance_tag == callback_instance_tag) { + if (form) { + SetLoadedForm(form); + CompleteFuture(handle, kConsentFormSuccess, "Success"); + } else if (error) { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), + error.localizedDescription.UTF8String); + } else { + CompleteFuture(handle, kConsentFormErrorUnknown, "An unknown error occurred."); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalIos::ShowConsentForm(FormParent parent) { + MutexLock lock(s_instance_mutex); + if (ShowConsentFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); + + if (!loaded_form_) { + CompleteFuture(handle, kConsentFormErrorInvalidOperation, + "You must call LoadConsentForm() prior to calling ShowConsentForm()."); + } else { + unsigned int callback_instance_tag; + callback_instance_tag = s_instance_tag; + + util::DispatchAsyncSafeMainQueue(^{ + MutexLock lock(s_instance_mutex); + if (!s_instance || s_instance_tag != callback_instance_tag) { + // Instance changed or was invalidated, don't call the iOS method any more. + return; + } + [loaded_form_ presentFromViewController:parent + completionHandler:^(NSError* _Nullable error) { + MutexLock lock(s_instance_mutex); + if (s_instance && s_instance_tag == callback_instance_tag) { + if (!error) { + CompleteFuture(handle, kConsentRequestSuccess); + } else { + CompleteFuture(handle, CppFormErrorFromIosFormError(error.code), + error.localizedDescription.UTF8String); + } + } + }]; + }); + } + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalIos::LoadAndShowConsentFormIfRequired(FormParent parent) { + MutexLock lock(s_instance_mutex); + if (LoadAndShowConsentFormIfRequiredLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); + + unsigned int callback_instance_tag; + callback_instance_tag = s_instance_tag; + + util::DispatchAsyncSafeMainQueue(^{ + MutexLock lock(s_instance_mutex); + if (!s_instance || s_instance_tag != callback_instance_tag) { + // Instance changed or was invalidated, don't call the iOS method any more. + return; + } + [UMPConsentForm + loadAndPresentIfRequiredFromViewController:parent + completionHandler:^(NSError* _Nullable error) { + MutexLock lock(s_instance_mutex); + if (s_instance && s_instance_tag == callback_instance_tag) { + if (!error) { + CompleteFuture(handle, kConsentRequestSuccess); + } else { + CompleteFuture(handle, + CppFormErrorFromIosFormError(error.code), + error.localizedDescription.UTF8String); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +PrivacyOptionsRequirementStatus ConsentInfoInternalIos::GetPrivacyOptionsRequirementStatus() { + UMPPrivacyOptionsRequirementStatus ios_status = + UMPConsentInformation.sharedInstance.privacyOptionsRequirementStatus; + switch (ios_status) { + case UMPPrivacyOptionsRequirementStatusRequired: + return kPrivacyOptionsRequirementStatusRequired; + case UMPPrivacyOptionsRequirementStatusNotRequired: + return kPrivacyOptionsRequirementStatusNotRequired; + case UMPPrivacyOptionsRequirementStatusUnknown: + return kPrivacyOptionsRequirementStatusUnknown; + default: + LogWarning("UMP: Unknown UMPPrivacyOptionsRequirementStatus returned by UMP iOS SDK: %d", + (int)ios_status); + return kPrivacyOptionsRequirementStatusUnknown; + } +} + +Future ConsentInfoInternalIos::ShowPrivacyOptionsForm(FormParent parent) { + MutexLock lock(s_instance_mutex); + if (ShowPrivacyOptionsFormLastResult().status() == kFutureStatusPending) { + // This operation is already in progress. + // Return a future with an error - this will not override the Fn entry. + SafeFutureHandle error_handle = CreateFuture(); + CompleteFuture(error_handle, kConsentFormErrorOperationInProgress); + return MakeFuture(futures(), error_handle); + } + + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); + unsigned int callback_instance_tag; + callback_instance_tag = s_instance_tag; + + util::DispatchAsyncSafeMainQueue(^{ + MutexLock lock(s_instance_mutex); + if (!s_instance || s_instance_tag != callback_instance_tag) { + // Instance changed or was invalidated, don't call the iOS method any more. + return; + } + [UMPConsentForm + presentPrivacyOptionsFormFromViewController:parent + completionHandler:^(NSError* _Nullable error) { + MutexLock lock(s_instance_mutex); + if (s_instance && s_instance_tag == callback_instance_tag) { + if (!error) { + CompleteFuture(handle, kConsentRequestSuccess); + } else { + CompleteFuture(handle, + CppFormErrorFromIosFormError(error.code), + error.localizedDescription.UTF8String); + } + } + }]; + }); + return MakeFuture(futures(), handle); +} + +bool ConsentInfoInternalIos::CanRequestAds() { + return (UMPConsentInformation.sharedInstance.canRequestAds == YES ? true : false); +} + +void ConsentInfoInternalIos::Reset() { [UMPConsentInformation.sharedInstance reset]; } + +} // namespace internal +} // namespace ump +} // namespace firebase diff --git a/ump/src/stub/consent_info_internal_stub.cc b/ump/src/stub/consent_info_internal_stub.cc new file mode 100644 index 0000000000..a0a4c4f4da --- /dev/null +++ b/ump/src/stub/consent_info_internal_stub.cc @@ -0,0 +1,165 @@ +/* + * 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. + */ + +#include "ump/src/stub/consent_info_internal_stub.h" + +#include "app/src/thread.h" + +namespace firebase { +namespace ump { +namespace internal { + +// This explicitly implements the constructor for the outer class, +// ConsentInfoInternal. +ConsentInfoInternal* ConsentInfoInternal::CreateInstance() { + return new ConsentInfoInternalStub(); +} + +ConsentInfoInternalStub::ConsentInfoInternalStub() + : consent_status_(kConsentStatusUnknown), + consent_form_status_(kConsentFormStatusUnknown), + privacy_options_requirement_status_( + kPrivacyOptionsRequirementStatusUnknown), + under_age_of_consent_(false), + debug_geo_(kConsentDebugGeographyDisabled) {} + +ConsentInfoInternalStub::~ConsentInfoInternalStub() {} + +Future ConsentInfoInternalStub::RequestConsentInfoUpdate( + const ConsentRequestParameters& params) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnRequestConsentInfoUpdate); + + // See the header file for an explanation of these default settings. + ConsentStatus new_consent_status = kConsentStatusObtained; + PrivacyOptionsRequirementStatus new_privacy_req = + kPrivacyOptionsRequirementStatusNotRequired; + // Simulate consent status based on debug geo. + if (params.debug_settings.debug_geography == kConsentDebugGeographyEEA) { + new_consent_status = kConsentStatusRequired; + } else if (params.debug_settings.debug_geography == + kConsentDebugGeographyNonEEA) { + new_consent_status = kConsentStatusNotRequired; + } + + consent_status_ = new_consent_status; + under_age_of_consent_ = params.tag_for_under_age_of_consent; + consent_form_status_ = + (under_age_of_consent_ || consent_status_ != kConsentStatusRequired) + ? kConsentFormStatusUnavailable + : kConsentFormStatusAvailable; + debug_geo_ = params.debug_settings.debug_geography; + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + + CompleteFuture(handle, kConsentRequestSuccess); + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalStub::LoadConsentForm() { + SafeFutureHandle handle = CreateFuture(kConsentInfoFnLoadConsentForm); + + if (consent_form_status_ != kConsentFormStatusAvailable) { + CompleteFuture(handle, kConsentFormErrorUnavailable); + return MakeFuture(futures(), handle); + } + CompleteFuture(handle, kConsentFormSuccess); + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalStub::ShowConsentForm(FormParent parent) { + SafeFutureHandle handle = CreateFuture(kConsentInfoFnShowConsentForm); + + consent_status_ = kConsentStatusObtained; + + if (debug_geo_ == kConsentDebugGeographyEEA) { + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusRequired; + } else if (debug_geo_ == kConsentDebugGeographyNonEEA) { + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + } else { // no debug option + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + } + + CompleteFuture(handle, kConsentRequestSuccess); + return MakeFuture(futures(), handle); +} + +Future ConsentInfoInternalStub::LoadAndShowConsentFormIfRequired( + FormParent parent) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnLoadAndShowConsentFormIfRequired); + + if (consent_status_ == kConsentStatusRequired && + consent_form_status_ != kConsentFormStatusAvailable) { + CompleteFuture(handle, kConsentFormErrorUnavailable); + return MakeFuture(futures(), handle); + } + + if (consent_status_ == kConsentStatusRequired) { + consent_status_ = kConsentStatusObtained; + if (debug_geo_ == kConsentDebugGeographyEEA) { + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusRequired; + } else if (debug_geo_ == kConsentDebugGeographyNonEEA) { + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + } else { // no debug option + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + } + } + CompleteFuture(handle, kConsentRequestSuccess); + return MakeFuture(futures(), handle); +} + +PrivacyOptionsRequirementStatus +ConsentInfoInternalStub::GetPrivacyOptionsRequirementStatus() { + return privacy_options_requirement_status_; +} + +Future ConsentInfoInternalStub::ShowPrivacyOptionsForm( + FormParent parent) { + SafeFutureHandle handle = + CreateFuture(kConsentInfoFnShowPrivacyOptionsForm); + + if (consent_status_ == kConsentStatusObtained) { + consent_status_ = kConsentStatusRequired; + privacy_options_requirement_status_ = + kPrivacyOptionsRequirementStatusNotRequired; + } + CompleteFuture(handle, kConsentRequestSuccess); + return MakeFuture(futures(), handle); +} + +bool ConsentInfoInternalStub::CanRequestAds() { + bool consent_status_ok = (consent_status_ == kConsentStatusObtained || + consent_status_ == kConsentStatusNotRequired); + bool privacy_options_ok = (privacy_options_requirement_status_ != + kPrivacyOptionsRequirementStatusUnknown); + return consent_status_ok && privacy_options_ok; +} + +void ConsentInfoInternalStub::Reset() { + consent_status_ = kConsentStatusUnknown; + consent_form_status_ = kConsentFormStatusUnknown; +} + +} // namespace internal +} // namespace ump +} // namespace firebase diff --git a/ump/src/stub/consent_info_internal_stub.h b/ump/src/stub/consent_info_internal_stub.h new file mode 100644 index 0000000000..9da0655e0d --- /dev/null +++ b/ump/src/stub/consent_info_internal_stub.h @@ -0,0 +1,84 @@ +/* + * 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. + */ + +#ifndef FIREBASE_UMP_SRC_STUB_CONSENT_INFO_INTERNAL_STUB_H_ +#define FIREBASE_UMP_SRC_STUB_CONSENT_INFO_INTERNAL_STUB_H_ + +#include "ump/src/common/consent_info_internal.h" + +namespace firebase { +namespace ump { +namespace internal { + +// The stub interface implements a few specific workflows, for testing: +// +// Before requesting: consent and privacy options requirement will be Unknown. +// +// After requesting: +// +// If debug_geography == EEA, consent will be Required, privacy options +// NotRequired. After calling ShowConsentForm() or +// LoadAndShowConsentFormIfRequired(), it will change to change to Obtained and +// privacy options will become Required, and when the privacy options form is +// shown, consent will go back to Required. +// +// If debug_geography == NonEEA, consent will be NotRequired. No privacy options +// form is required. +// +// If debug_geography == Disabled, consent will be Obtained and privacy options +// will be NotRequired. +// +// If tag_for_under_age_of_consent = true, LoadConsentForm and +// LoadAndShowConsentFormIfRequired will fail with kConsentFormErrorUnavailable. +// +// CanRequestAds returns true if consent is NotRequired or Obtained. +class ConsentInfoInternalStub : public ConsentInfoInternal { + public: + ConsentInfoInternalStub(); + ~ConsentInfoInternalStub() override; + + ConsentStatus GetConsentStatus() override { return consent_status_; } + ConsentFormStatus GetConsentFormStatus() override { + return consent_form_status_; + } + + Future RequestConsentInfoUpdate( + const ConsentRequestParameters& params) override; + Future LoadConsentForm() override; + Future ShowConsentForm(FormParent parent) override; + + Future LoadAndShowConsentFormIfRequired(FormParent parent) override; + + PrivacyOptionsRequirementStatus GetPrivacyOptionsRequirementStatus() override; + Future ShowPrivacyOptionsForm(FormParent parent) override; + + bool CanRequestAds() override; + + void Reset() override; + + private: + ConsentStatus consent_status_; + ConsentFormStatus consent_form_status_; + PrivacyOptionsRequirementStatus privacy_options_requirement_status_; + ConsentDebugGeography debug_geo_; + bool under_age_of_consent_; +}; + +} // namespace internal +} // namespace ump +} // namespace firebase + +#endif // FIREBASE_SRC_STUB_CONSENT_INFO_INTERNAL_STUB_H_ diff --git a/ump/src_java/com/google/firebase/ump/internal/cpp/ConsentInfoHelper.java b/ump/src_java/com/google/firebase/ump/internal/cpp/ConsentInfoHelper.java new file mode 100644 index 0000000000..637d24fae2 --- /dev/null +++ b/ump/src_java/com/google/firebase/ump/internal/cpp/ConsentInfoHelper.java @@ -0,0 +1,312 @@ +/* + * 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.ump.internal.cpp; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import com.google.android.ump.ConsentDebugSettings; +import com.google.android.ump.ConsentForm; +import com.google.android.ump.ConsentForm.OnConsentFormDismissedListener; +import com.google.android.ump.ConsentInformation; +import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateFailureListener; +import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateSuccessListener; +import com.google.android.ump.ConsentInformation.PrivacyOptionsRequirementStatus; +import com.google.android.ump.ConsentRequestParameters; +import com.google.android.ump.FormError; +import com.google.android.ump.UserMessagingPlatform; +import com.google.android.ump.UserMessagingPlatform.OnConsentFormLoadFailureListener; +import com.google.android.ump.UserMessagingPlatform.OnConsentFormLoadSuccessListener; +import java.util.ArrayList; + +/** + * Helper class to make interactions between the UMP C++ wrapper and the Android UMP API. + */ +public class ConsentInfoHelper { + // C++ nullptr for use with the callbacks. + private static final long CPP_NULLPTR = 0; + + // Synchronization object for thread safe access to: + private final Object mLock = new Object(); + // Pointer to the internal ConsentInfoInternalAndroid C++ object. + // This can be reset back to 0 by calling disconnect(). + private long mInternalPtr = 0; + // The Activity that this was initialized with. + private Activity mActivity = null; + // The loaded consent form, if any. + private ConsentForm mConsentForm = null; + + // Create our own local passthrough version of these enum object values + // as integers, to make it easier for the C++ SDK to access them. + public static final int PRIVACY_OPTIONS_REQUIREMENT_UNKNOWN = + PrivacyOptionsRequirementStatus.UNKNOWN.ordinal(); + public static final int PRIVACY_OPTIONS_REQUIREMENT_REQUIRED = + PrivacyOptionsRequirementStatus.REQUIRED.ordinal(); + public static final int PRIVACY_OPTIONS_REQUIREMENT_NOT_REQUIRED = + PrivacyOptionsRequirementStatus.NOT_REQUIRED.ordinal(); + + // Enum values for tracking which function we are calling back. + // Ensure these are incremental starting at 0. + // These don't have to match ConsentInfoFn, as the C++ code will + // use these Java enums directly. + public static final int FUNCTION_REQUEST_CONSENT_INFO_UPDATE = 0; + public static final int FUNCTION_LOAD_CONSENT_FORM = 1; + public static final int FUNCTION_SHOW_CONSENT_FORM = 2; + public static final int FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED = 3; + public static final int FUNCTION_SHOW_PRIVACY_OPTIONS_FORM = 4; + public static final int FUNCTION_COUNT = 5; + + public ConsentInfoHelper(long consentInfoInternalPtr, Activity activity) { + synchronized (mLock) { + mInternalPtr = consentInfoInternalPtr; + mActivity = activity; + // Test the callbacks and fail quickly if something's wrong. + completeFuture(-1, CPP_NULLPTR, CPP_NULLPTR, 0, null); + } + } + + public int getConsentStatus() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.getConsentStatus(); + } + + public void requestConsentInfoUpdate(final long futureHandle, boolean tagForUnderAgeOfConsent, + int debugGeography, ArrayList debugIdList) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_REQUEST_CONSENT_INFO_UPDATE; + + ConsentDebugSettings.Builder debugSettingsBuilder = null; + + // Only create and use debugSettingsBuilder if a debug option is set. + if (debugGeography != ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED) { + debugSettingsBuilder = + new ConsentDebugSettings.Builder(mActivity).setDebugGeography(debugGeography); + } + if (debugIdList != null && debugIdList.size() > 0) { + if (debugSettingsBuilder == null) { + debugSettingsBuilder = new ConsentDebugSettings.Builder(mActivity); + } + for (int i = 0; i < debugIdList.size(); i++) { + debugSettingsBuilder = debugSettingsBuilder.addTestDeviceHashedId(debugIdList.get(i)); + } + } + ConsentRequestParameters.Builder paramsBuilder = + new ConsentRequestParameters.Builder().setTagForUnderAgeOfConsent(tagForUnderAgeOfConsent); + + if (debugSettingsBuilder != null) { + paramsBuilder = paramsBuilder.setConsentDebugSettings(debugSettingsBuilder.build()); + } + + final ConsentRequestParameters params = paramsBuilder.build(); + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + consentInfo.requestConsentInfoUpdate(mActivity, params, + new OnConsentInfoUpdateSuccessListener() { + @Override + public void onConsentInfoUpdateSuccess() { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } + } + }, + new OnConsentInfoUpdateFailureListener() { + @Override + public void onConsentInfoUpdateFailure(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + }); + } + }); + } + + public void loadConsentForm(final long futureHandle) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_LOAD_CONSENT_FORM; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.loadConsentForm(mActivity, + new OnConsentFormLoadSuccessListener() { + @Override + public void onConsentFormLoadSuccess(ConsentForm form) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + mConsentForm = form; + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } + } + }, + new OnConsentFormLoadFailureListener() { + @Override + public void onConsentFormLoadFailure(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + mConsentForm = null; + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + }); + } + }); + } + + public boolean showConsentForm(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return false; + } + final int functionId = FUNCTION_SHOW_CONSENT_FORM; + ConsentForm consentForm; + synchronized (mLock) { + if (mConsentForm == null) { + // Consent form was not loaded, return an error. + return false; + } + consentForm = mConsentForm; + mConsentForm = null; + } + final ConsentForm consentFormForThread = consentForm; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + consentFormForThread.show(activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + // Consent form is loaded. + return true; + } + + public void loadAndShowConsentFormIfRequired(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_LOAD_AND_SHOW_CONSENT_FORM_IF_REQUIRED; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + } + + public int getPrivacyOptionsRequirementStatus() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.getPrivacyOptionsRequirementStatus().ordinal(); + } + + public void showPrivacyOptionsForm(final long futureHandle, final Activity activity) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + } + final int functionId = FUNCTION_SHOW_PRIVACY_OPTIONS_FORM; + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + UserMessagingPlatform.showPrivacyOptionsForm( + activity, new OnConsentFormDismissedListener() { + @Override + public void onConsentFormDismissed(FormError formError) { + synchronized (mLock) { + if (mInternalPtr == 0) + return; + if (formError == null) { + completeFuture(functionId, mInternalPtr, futureHandle, 0, null); + } else { + completeFuture(functionId, mInternalPtr, futureHandle, formError.getErrorCode(), + formError.getMessage()); + } + } + } + }); + } + }); + } + + public boolean canRequestAds() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.canRequestAds(); + } + + public boolean isConsentFormAvailable() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + return consentInfo.isConsentFormAvailable(); + } + + public void reset() { + ConsentInformation consentInfo = UserMessagingPlatform.getConsentInformation(mActivity); + consentInfo.reset(); + } + + /** Disconnect the helper from the native object. */ + public void disconnect() { + synchronized (mLock) { + mInternalPtr = CPP_NULLPTR; + } + } + public static native void completeFuture( + int futureFn, long nativeInternalPtr, long futureHandle, int errorCode, String errorMessage); +} diff --git a/ump/ump_resources/AndroidManifest.xml b/ump/ump_resources/AndroidManifest.xml new file mode 100644 index 0000000000..66236bd0d6 --- /dev/null +++ b/ump/ump_resources/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/ump/ump_resources/build.gradle b/ump/ump_resources/build.gradle new file mode 100644 index 0000000000..ca3de615b7 --- /dev/null +++ b/ump/ump_resources/build.gradle @@ -0,0 +1,72 @@ +// 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. + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.google.gms:google-services:4.4.1' + } +} +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + compileSdkVersion 34 + buildToolsVersion '32.0.0' + + defaultConfig { + minSdkVersion 23 + targetSdkVersion 34 + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java { + srcDirs = ['../src_java/com/google/firebase/ump/internal/cpp'] + } + } + } +} + +dependencies { + implementation platform('com.google.firebase:firebase-bom:33.15.0') + implementation 'com.google.firebase:firebase-analytics' + implementation 'com.google.android.ump:user-messaging-platform:2.2.0' +} + +afterEvaluate { + generateReleaseBuildConfig.enabled = false + project.tasks.withType(com.android.build.gradle.internal.tasks.CheckAarMetadataTask) { + enabled = false + } +} + +apply from: "$rootDir/android_build_files/extract_and_dex.gradle" +extractAndDexAarFile('ump_resources')