diff --git a/.github/workflows/functional-tests.yml b/.github/workflows/android-emulator-tests.yml similarity index 81% rename from .github/workflows/functional-tests.yml rename to .github/workflows/android-emulator-tests.yml index f8f4afb5be..aaacbf56e7 100644 --- a/.github/workflows/functional-tests.yml +++ b/.github/workflows/android-emulator-tests.yml @@ -1,10 +1,8 @@ -name: Run Functional Tests +name: Run tests on android emulator on: workflow_dispatch: workflow_call: - schedule: - - cron: '0 3 * * *' pull_request: branches: [ main ] @@ -62,7 +60,7 @@ jobs: disable-animations: false script: echo "Generated AVD snapshot for caching." - - name: Run Functional Tests + - name: Run tests on android emulator uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} @@ -77,20 +75,8 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: sdk-functional-test-results + name: android-emulator-test-results path: | embrace-android-sdk/build/reports/androidTests/connected test_failure emulator_logcat.log - - - name: Notify Slack of Test Failure - if: failure() - uses: slackapi/slack-github-action@v1.27.0 - with: - payload: | - { - "failed_action_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/.github/workflows/build-example-app.yml b/.github/workflows/build-example-app.yml index ca5d840d82..6e41e070cc 100644 --- a/.github/workflows/build-example-app.yml +++ b/.github/workflows/build-example-app.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true jobs: - gradle-test: + build-example-app: runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -31,6 +31,6 @@ jobs: uses: gradle/actions/setup-gradle@v4 # Build the entire project, run the tests, and run all static analysis - - name: Gradle Build + - name: Build example app working-directory: examples/ExampleApp run: ./gradlew bundleRelease diff --git a/.github/workflows/bump-snapshot-version.yml b/.github/workflows/bump-snapshot-version.yml new file mode 100644 index 0000000000..d3774de6d1 --- /dev/null +++ b/.github/workflows/bump-snapshot-version.yml @@ -0,0 +1,49 @@ +name: Bump snapshot version + +on: + workflow_call: + inputs: + next_version: + required: true + type: string + workflow_dispatch: + inputs: + next_version: + description: 'Next version. Specify , e.g. 6.4 (Do NOT include -SNAPSHOT, this will be added automatically)' + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + bump-snapshot-version: + name: Bump snapshot version + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + + - name: Checkout SDK + uses: actions/checkout@v4 + + - name: Set next version + run: sed -i -r "s#version=([^\']+)#version=${{ inputs.next_version }}.0-SNAPSHOT#" gradle.properties + + - name: Create PR + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name 'embrace-ci[bot]' + git config --global user.email 'embrace-ci@users.noreply.github.com' + + branch=sdk-version-update-${{ github.run_id }} + git checkout -b $branch + + git add gradle.properties + git commit -m "CI/CD: set next version: ${{ inputs.next_version }}.0-SNAPSHOT" + git push -f origin $branch + gh pr create \ + --base main \ + --head $branch \ + --title "CI/CD: set next version: ${{ inputs.next_version }}.0-SNAPSHOT" \ + --body "This is an autogenerated PR, created by the bump-snapshot-version.yml workflow" diff --git a/.github/workflows/ci-gradle.yml b/.github/workflows/ci-gradle.yml index 1b50d679b9..baf84934d1 100644 --- a/.github/workflows/ci-gradle.yml +++ b/.github/workflows/ci-gradle.yml @@ -14,7 +14,7 @@ concurrency: jobs: gradle-test: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: Checkout Branch uses: actions/checkout@v4 @@ -43,14 +43,14 @@ jobs: # Build the entire project, run the tests, and run all static analysis - name: Gradle Build - run: ./gradlew assembleRelease testReleaseUnitTest detekt lint apiCheck --stacktrace + run: ./gradlew assembleRelease check --stacktrace - name: Archive Test Results if: ${{ always() }} uses: actions/upload-artifact@v4 with: - name: android-sdk-test-results - path: embrace-android-sdk/build/reports/tests/ + name: test-results + path: '**/build/reports/tests/**' - name: Run Kover Code Coverage run: ./gradlew koverXmlReportRelease diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml new file mode 100644 index 0000000000..c1c7dbcfe7 --- /dev/null +++ b/.github/workflows/create-release-branch.yml @@ -0,0 +1,41 @@ +name: Create Release Branch + +on: + workflow_call: + inputs: + rc_version: + required: true + type: string + workflow_dispatch: + inputs: + rc_version: + description: 'Version to release. Specify , e.g. 7.1.2. A new branch called "release/" will be created where the release-specific changes will be committed.' + required: true + +permissions: + contents: write + +jobs: + create-release-branch: + name: Create Release Branch + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Configure git + run: | + git config --global user.name 'embrace-ci[bot]' + git config --global user.email 'embrace-ci@users.noreply.github.com' + + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Release Branch "release/${{ inputs.rc_version }}" + run: | + git checkout -b release/${{ inputs.rc_version }} + + - name: Set version in gradle.properties + run: | + sed -i -r "s#version = ([^\']+)#version=${{ inputs.rc_version }}#" gradle.properties + git add gradle.properties + git commit -m "CI/CD: change version to be released: ${{ inputs.rc_version }}" + git push --set-upstream origin release/${{ inputs.rc_version }} diff --git a/.github/workflows/embrace-gradle-plugin-tests.yml b/.github/workflows/embrace-gradle-plugin-tests.yml deleted file mode 100644 index b57090204f..0000000000 --- a/.github/workflows/embrace-gradle-plugin-tests.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: "Run embrace-gradle-plugin-tests" - -on: - pull_request: - workflow_dispatch: - -env: - ANDROID_BUILD_TOOLS_HOME: "/usr/local/lib/android/sdk/build-tools/35.0.0" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - runTests: - name: "Run Gradle Tests" - runs-on: ubuntu-latest - timeout-minutes: 60 - steps: - - uses: actions/checkout@v4 - - - name: Install Java - uses: actions/setup-java@v4 - with: - distribution: 'adopt' - java-version: 17 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Cache NDK - id: ndk-cache - uses: actions/cache@v4 - with: - path: /usr/local/lib/android/sdk/ndk/27.0.12077973 - key: ndk-cache - - - name: Install NDK - if: steps.ndk-cache.outputs.cache-hit != 'true' - run: | - echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.12077973" - echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "cmake;3.10.2.4988404" - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20.x - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - path: ./embrace-swazzler3 - token: ${{ secrets.GH_ANDROID_SDK_TOKEN }} - - - name: "Run Integration Tests" - working-directory: ./embrace-swazzler3 - run: ./gradlew :embrace-gradle-plugin-integration-tests:test --stacktrace - - - name: "Test Results" - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: gradle-plugin-test-results - path: | - ./embrace-swazzler3/embrace-gradle-plugin-integration-tests/build/reports/tests/test diff --git a/.github/workflows/generate_baseline_profile.yml b/.github/workflows/generate-baseline-profile.yml similarity index 85% rename from .github/workflows/generate_baseline_profile.yml rename to .github/workflows/generate-baseline-profile.yml index 6c88b4f866..6556829de7 100644 --- a/.github/workflows/generate_baseline_profile.yml +++ b/.github/workflows/generate-baseline-profile.yml @@ -35,25 +35,14 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Read SDK version + - name: Read version run: | # Read the version from gradle.properties version=$(grep '^version' gradle.properties | cut -d'=' -f2 | tr -d '[:space:]') # Set the version as an environment variable echo "ANDROID_SDK_VERSION=${version}" >> "$GITHUB_ENV" - - name: Publish SDK to Maven local - run: ./gradlew pTML - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - token: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - path: './embrace-swazzler3' - - - name: Publish embrace gradle plugin to Maven local - working-directory: ./embrace-swazzler3 + - name: Publish to Maven local run: ./gradlew pTML - name: Checkout Android SDK Benchmark @@ -70,7 +59,7 @@ jobs: - name: Generate Baseline Profile working-directory: android-sdk-benchmark - run: ./gradlew -Pswazzler_version=${{ env.ANDROID_SDK_VERSION }} :macrobenchmark:pixel6Api34BaselineProfileAndroidTest + run: ./gradlew :macrobenchmark:pixel6Api34BaselineProfileAndroidTest -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1 diff --git a/.github/workflows/pre-release-workflow.yml b/.github/workflows/pre-release-workflow.yml index cbb319bc09..008ce9c620 100644 --- a/.github/workflows/pre-release-workflow.yml +++ b/.github/workflows/pre-release-workflow.yml @@ -3,32 +3,32 @@ name: Prepare New Release on: workflow_dispatch: inputs: - version_to_release: - description: 'Version to release. Specify only, without the patch number, e.g. 6.3. A new branch called "release/" will be created where the release-specific changes will be committed.' + rc_version: + description: 'Version to release. Specify e.g. 7.1.2. A new branch called "release/" will be created where the release-specific changes will be committed.' required: true next_version: - description: 'Next version. Specify , e.g. 6.4 (Do NOT include -SNAPSHOT, will be added automatically)' + description: 'Next version. Specify , e.g. 7.2.0 (Do NOT include -SNAPSHOT, will be added automatically)' required: true jobs: - release-branch: - name: Create Release Branches - uses: ./.github/workflows/rc-release-branch.yml + create-release-branch: + name: Create Release Branch + uses: ./.github/workflows/create-release-branch.yml secrets: inherit with: - version_to_release: ${{ github.event.inputs.version_to_release }} + rc_version: ${{ inputs.rc_version }} - update-main-versions: - name: Update Main Versions - uses: ./.github/workflows/update-main-versions.yml + bump-snapshot-version: + name: Bump Snapshot Version + uses: ./.github/workflows/bump-snapshot-version.yml secrets: inherit with: - next_version: ${{ github.event.inputs.next_version }} + next_version: ${{ inputs.next_version }} - release-candidate: - name: Build Release Candidates - needs: release-branch - uses: ./.github/workflows/build-rc-workflow.yml + upload-artifacts-to-sonatype: + name: Upload RC artifacts to Sonatype + needs: create-release-branch + uses: ./.github/workflows/upload-artifacts-to-sonatype.yml secrets: inherit with: - version_of_rc: ${{ github.event.inputs.version_to_release }} \ No newline at end of file + rc_version: ${{ inputs.rc_version }} diff --git a/.github/workflows/publish-api-docs.yml b/.github/workflows/publish-api-docs.yml index f09bdeeca2..df5ead7828 100644 --- a/.github/workflows/publish-api-docs.yml +++ b/.github/workflows/publish-api-docs.yml @@ -1,23 +1,16 @@ -name: Publish API Docs +name: Publish API Docs to GitHub Pages on: workflow_call: inputs: - version_to_release: - required: true - type: string - patch_version_of_release: + rc_version: required: true type: string workflow_dispatch: inputs: - version_to_release: - description: 'SDK version these docs are for. Specify only, without the patch number, e.g. 6.3. It will use the branch "release/".' - required: true - patch_version_of_release: - description: 'The patch version for this release, i.e. the third number per semantic versioning . If it is not a hotfix, use the default of 0' + rc_version: + description: 'SDK version these docs are for. Specify e.g. 7.1.2. It will use the branch "release/".' required: true - default: '0' permissions: contents: write @@ -35,7 +28,7 @@ jobs: - name: Checkout SDK uses: actions/checkout@v4 with: - ref: release/${{ github.event.inputs.version_to_release }} + ref: release/${{ inputs.rc_version }} fetch-depth: 0 - name: Setup Java @@ -50,7 +43,7 @@ jobs: - name: Generate Documentation run: ./gradlew dokkaHtmlMultiModule clean --no-build-cache --no-configuration-cache - - name: Publish gh-pages + - name: Publish GitHub Pages run: | mv build/dokka/htmlMultiModule .docs-newly-generated # new docs generated by previous step git checkout gh-pages @@ -58,13 +51,13 @@ jobs: mv .docs-newly-generated docs date > docs/version.txt echo ${{ github.sha }} >> docs/version.txt - echo ${{ github.event.inputs.version_to_release }}.${{ github.event.inputs.patch_version_of_release }} >> docs/version.txt + echo ${{ inputs.rc_version }} >> docs/version.txt git add -f docs git config --global user.name "embrace-ci" git config --global user.email "embrace-ci@users.noreply.github.com" - git commit --allow-empty --message 'CI/CD: Automatically generated documentation for ${{ github.event.inputs.version_to_release }}.${{ github.event.inputs.patch_version_of_release }}' docs/ + git commit --allow-empty --message 'CI/CD: Automatically generated documentation for ${{ inputs.rc_version }}' docs/ git push --force origin gh-pages - - name: Record SDK Version History (${{ github.event.inputs.version_to_release }}.${{ github.event.inputs.patch_version_of_release }}) + - name: Record Version History (${{ inputs.rc_version }}) run: | - curl -X POST ${{ vars.SDK_VERSION_URL }}/android/version/ -H 'X-Embrace-CI: ${{ secrets.SDK_VERSION_TOKEN }}' -H 'Content-Type: application/json' -d '{"version": "${{ github.event.inputs.version_to_release }}.${{ github.event.inputs.patch_version_of_release }}"}' + curl -X POST ${{ vars.SDK_VERSION_URL }}/android/version/ -H 'X-Embrace-CI: ${{ secrets.SDK_VERSION_TOKEN }}' -H 'Content-Type: application/json' -d '{"version": "${{ inputs.rc_version }}"}' diff --git a/.github/workflows/rc-release-branch.yml b/.github/workflows/rc-release-branch.yml deleted file mode 100644 index 4eccf76774..0000000000 --- a/.github/workflows/rc-release-branch.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Create Release Branches - -on: - workflow_call: - inputs: - version_to_release: - required: true - type: string - workflow_dispatch: - inputs: - version_to_release: - description: 'Version to release. Specify only, without the patch number, e.g. 6.3. A new branch called "release/" will be created where the release-specific changes will be committed.' - required: true - -permissions: - contents: write - -jobs: - release-branch: - name: Create Release Branches - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - name: Configure git - run: | - git config --global user.name 'embrace-ci[bot]' - git config --global user.email 'embrace-ci@users.noreply.github.com' - - - name: Checkout SDK - uses: actions/checkout@v4 - - - name: Create SDK Release Branch "release/${{ github.event.inputs.version_to_release }}" - run: | - git checkout -b release/${{ github.event.inputs.version_to_release }} - - - name: Set version in gradle.properties - run: | - sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.version_to_release }}.0#" gradle.properties - git add gradle.properties - git commit -m "CI/CD: change version to be released: ${{ github.event.inputs.version_to_release }}.0" - git push --set-upstream origin release/${{ github.event.inputs.version_to_release }} - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - token: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - - - name: Create embrace gradle plugin Release Branch "release/${{ github.event.inputs.version_to_release }}" - run: | - git checkout -b release/${{ github.event.inputs.version_to_release }} - - - name: Set version in gradle.properties - run: | - sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.version_to_release }}.0#" gradle.properties - git add gradle.properties - git commit -m "CI/CD: change version to be released: ${{ github.event.inputs.version_to_release }}.0" - git push --set-upstream origin release/${{ github.event.inputs.version_to_release }} diff --git a/.github/workflows/rc-update-test-apps.yml b/.github/workflows/rc-update-test-apps.yml deleted file mode 100644 index 8410c24610..0000000000 --- a/.github/workflows/rc-update-test-apps.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Pre-Release - Update Test Apps with new Snapshot - -on: - workflow_dispatch: - inputs: - next_version: - description: 'Next version. Specify , e.g. 6.4 (Do NOT include -SNAPSHOT, will be added automatically)' - required: true - workflow_call: - inputs: - next_version: - required: true - type: string - -jobs: - update-apps: - name: Update Apps Repo - strategy: - fail-fast: false - matrix: - repo: [ embrace-io/android-sdk-benchmark, embrace-io/android-test-suite, embrace-io/ndk-test-app-ndkbuild, embrace-io/ndk-test-app-custombuild, embrace-io/ndk-test-app, embrace-io/android-size-measure ] - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - name: Configure git - run: | - git config --global user.name 'embrace-ci[bot]' - git config --global user.email 'embrace-ci@users.noreply.github.com' - - - name: Checkout App - uses: actions/checkout@v4 - with: - repository: ${{ matrix.repo }} - token: ${{ secrets.GH_ANDROID_SDK_TOKEN }} # NOTE: write embrace-io/android-sdk-benchmark, embrace-io/android-test-suite, embrace-io/ndk-test-app-ndkbuild, embrace-io/ndk-test-app-custombuild, embrace-io/ndk-test-app, embrace-io/android-size-measure - - - name: Set next SDK version - run: | - git checkout main - sed -i -r "s#swazzler_version = ([^\']+)#swazzler_version = ${{ github.event.inputs.next_version }}.0-SNAPSHOT#" gradle.properties - git add gradle.properties - git commit -m "CI/CD: set next version: ${{ github.event.inputs.next_version }}.0-SNAPSHOT" - git push diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 878728f457..37b5b7927e 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -1,4 +1,4 @@ -name: Release - Release RC and Update Documentation +name: Release RC and Update Docs env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_TOKEN_USER }} @@ -12,26 +12,22 @@ env: on: workflow_dispatch: inputs: - version_to_release: - description: 'SDK version this workflow run will release. Specify only, without the patch number, e.g. 6.3. It will use the branch "release/".' + rc_version: + description: 'SDK version this workflow run will release. Specify e.g. 7.1.2. It will use the branch "release/".' required: true - patch_version_of_release: - description: 'The patch version for this release, i.e. the third number per semantic versioning . If it is not a hotfix, use the default of 0' - required: true - default: '0' permissions: contents: write jobs: - release: + release-to-maven-central: runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Checkout SDK uses: actions/checkout@v4 with: - ref: release/${{ github.event.inputs.version_to_release }} + ref: release/${{ inputs.rc_version }} fetch-depth: 0 - name: Setup Java @@ -43,26 +39,13 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Gradlew Release SDK to Maven Central - run: | - ./gradlew findSonatypeStagingRepository releaseSonatypeStagingRepository -Dorg.gradle.parallel=false --no-build-cache --no-configuration-cache --stacktrace - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - ref: release/${{ github.event.inputs.version_to_release }} - token: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - - - name: Release embrace gradle plugin to Maven Central + - name: Release to Maven Central run: | ./gradlew findSonatypeStagingRepository releaseSonatypeStagingRepository -Dorg.gradle.parallel=false --no-build-cache --no-configuration-cache --stacktrace publish-api-docs: - name: Publish API Docs + name: Publish API Docs to GitHub Pages uses: ./.github/workflows/publish-api-docs.yml secrets: inherit with: - version_to_release: ${{ github.event.inputs.version_to_release }} - patch_version_of_release: ${{ github.event.inputs.patch_version_of_release }} - + rc_version: ${{ inputs.rc_version }} diff --git a/.github/workflows/update-main-versions.yml b/.github/workflows/update-main-versions.yml deleted file mode 100644 index 368e8a6705..0000000000 --- a/.github/workflows/update-main-versions.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Update Trunk Versions - -on: - workflow_call: - inputs: - next_version: - required: true - type: string - workflow_dispatch: - inputs: - next_version: - description: 'Next version. Specify , e.g. 6.4 (Do NOT include -SNAPSHOT, will be added automatically)' - required: true - -permissions: - contents: write - pull-requests: write - -jobs: - update-main-versions: - name: Update Trunk Versions - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - - name: Checkout SDK - uses: actions/checkout@v4 - - - name: Set next SDK version - run: sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.next_version }}.0-SNAPSHOT#" gradle.properties - - - name: Create SDK version update PR - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config --global user.name 'embrace-ci[bot]' - git config --global user.email 'embrace-ci@users.noreply.github.com' - - branch=sdk-version-update-${{ github.run_id }} - git checkout -b $branch - - git add gradle.properties - git commit -m "CI/CD: set next version: ${{ github.event.inputs.next_version }}.0-SNAPSHOT" - git push -f origin $branch - gh pr create \ - --base main \ - --head $branch \ - --title "CI/CD: set next version: ${{ github.event.inputs.next_version }}.0-SNAPSHOT" \ - --body "This is an autogenerated PR, created by the update-main-versions.yml workflow" - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - token: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - - - name: Set Next embrace gradle plugin Version - run: sed -i -r "s#version = ([^\']+)#version = ${{ github.event.inputs.next_version }}.0-SNAPSHOT#" gradle.properties - - - name: Create embrace gradle plugin version update PR - env: - GITHUB_TOKEN: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - run: | - git config --global user.name 'embrace-ci[bot]' - git config --global user.email 'embrace-ci@users.noreply.github.com' - - git add gradle.properties - git commit -m "CI/CD: set next version: ${{ github.event.inputs.next_version }}.0-SNAPSHOT" - git push origin main diff --git a/.github/workflows/build-rc-workflow.yml b/.github/workflows/upload-artifacts-to-sonatype.yml similarity index 51% rename from .github/workflows/build-rc-workflow.yml rename to .github/workflows/upload-artifacts-to-sonatype.yml index afe1c55250..5ffec8f371 100644 --- a/.github/workflows/build-rc-workflow.yml +++ b/.github/workflows/upload-artifacts-to-sonatype.yml @@ -1,4 +1,4 @@ -name: Build Release Candidates +name: Upload artifacts to Sonatype env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_TOKEN_USER }} @@ -12,17 +12,13 @@ env: on: workflow_call: inputs: - version_of_rc: + rc_version: required: true type: string workflow_dispatch: inputs: - version_of_rc: - description: 'Version to create new RC for. Specify only, e.g. 6.3' - required: true - patch_number: - description: 'Patch number. e.g. for 6.3.1, use "1". Defaults to 0' - default: '0' + rc_version: + description: 'Version to create new RC for. Specify , e.g. 7.1.2' required: true permissions: @@ -44,10 +40,10 @@ jobs: git config --global user.name 'embrace-ci[bot]' git config --global user.email 'embrace-ci@users.noreply.github.com' - - name: Checkout SDK + - name: Checkout uses: actions/checkout@v4 with: - ref: release/${{ inputs.version_of_rc }} + ref: release/${{ inputs.rc_version }} - name: Setup Java uses: actions/setup-java@v4 @@ -58,36 +54,12 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Generate SDK RC - Publish and close Sonatype repository + - name: Publish and close Sonatype repository run: | ./gradlew clean publishReleasePublicationToSonatype closeSonatypeStagingRepository -Dorg.gradle.parallel=false --no-build-cache --no-configuration-cache --stacktrace - - name: Archive Test Results - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: android-sdk-test-results - path: embrace-android-sdk/build/reports/tests/ - - - name: Set version tag in SDK - run: | - git push origin :refs/tags/${{ inputs.version_of_rc }}.${{ inputs.patch_number }} - git tag -f ${{ inputs.version_of_rc }}.${{ inputs.patch_number }} - git push origin --tags - - - name: Checkout embrace gradle plugin - uses: actions/checkout@v4 - with: - repository: embrace-io/embrace-swazzler3 - ref: release/${{ inputs.version_of_rc }} - token: ${{ secrets.GH_EMBRACE_SWAZZLER3_TOKEN }} - - - name: Generate embrace gradle plugin RC - Publish and Close repository - run: | - ./gradlew clean publishToSonatype closeSonatypeStagingRepository -Dorg.gradle.parallel=false --no-build-cache --no-configuration-cache --stacktrace - - - name: Set version tag in embrace gradle plugin + - name: Publish git tag run: | - git push origin :refs/tags/${{ inputs.version_of_rc }}.${{ inputs.patch_number }} - git tag -f ${{ inputs.version_of_rc }}.${{ inputs.patch_number }} + git push origin :refs/tags/${{ inputs.rc_version }} + git tag -f ${{ inputs.rc_version }} git push origin --tags diff --git a/build.gradle.kts b/build.gradle.kts index 60a6b28338..5c59f31f47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.dokka.gradle.DokkaTaskPartial plugins { kotlin("android") apply false + kotlin("jvm") apply false alias(libs.plugins.google.ksp) apply false id("com.android.library") apply false alias(libs.plugins.nexus.publish) diff --git a/buildSrc/src/main/kotlin/io/embrace/internal/EmbraceBuildLogicExtension.kt b/buildSrc/src/main/kotlin/io/embrace/internal/EmbraceBuildLogicExtension.kt index 1df6bb450a..6957ff5a65 100644 --- a/buildSrc/src/main/kotlin/io/embrace/internal/EmbraceBuildLogicExtension.kt +++ b/buildSrc/src/main/kotlin/io/embrace/internal/EmbraceBuildLogicExtension.kt @@ -19,7 +19,7 @@ abstract class EmbraceBuildLogicExtension(objectFactory: ObjectFactory) { /** * Whether this module contains an Android library or not. */ - val androidLibrary: Property = + val androidModule: Property = objectFactory.property(Boolean::class.java).convention(true) /** diff --git a/embrace-bytecode-instrumentation-tests/.gitignore b/embrace-bytecode-instrumentation-tests/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/embrace-bytecode-instrumentation-tests/README.md b/embrace-bytecode-instrumentation-tests/README.md new file mode 100644 index 0000000000..90bc94207d --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/README.md @@ -0,0 +1,11 @@ +# Embrace Gradle Plugin Test Fixtures + +This module contains test fixture classes which are instrumented in unit tests. The bytecode is then compared against +known good output to confirm the functionality is working. + +To add a new test case you should: + +1. Create a new JVM class +2. Add the class to the parameterized `InstrumentedBytecodeTest` and run the test using the `defaultFactory` (this does not manipulate bytecode) +3. Create a resource named `$clz.simpleName_expected.txt` containing the default bytecode representation +4. Alter the test so that the factory returns the `ClassVisitor` that will manipulate its bytecode diff --git a/embrace-bytecode-instrumentation-tests/build.gradle.kts b/embrace-bytecode-instrumentation-tests/build.gradle.kts new file mode 100644 index 0000000000..2dd679dfd7 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/build.gradle.kts @@ -0,0 +1,44 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + kotlin("android") + id("io.embrace.internal.build-logic") +} + +embrace { + productionModule.set(false) + jvmTarget.set(JavaVersion.VERSION_11) +} + +android { + namespace = "io.embrace.android.gradle.test.fixtures" + compileSdk = 34 + defaultConfig.minSdk = 21 + + buildTypes { + release { + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + allWarningsAsErrors.set(true) + } + } +} + +dependencies { + implementation(libs.okhttp) + implementation(libs.appcompat) + + testImplementation(libs.agp.api) + testImplementation(libs.junit) + testImplementation(project(":embrace-gradle-plugin")) + testImplementation(libs.asm.util) + testImplementation(libs.gradle.test.kit) +} diff --git a/embrace-bytecode-instrumentation-tests/config/detekt/baseline.xml b/embrace-bytecode-instrumentation-tests/config/detekt/baseline.xml new file mode 100644 index 0000000000..05308663d1 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/config/detekt/baseline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/embrace-bytecode-instrumentation-tests/lint-baseline.xml b/embrace-bytecode-instrumentation-tests/lint-baseline.xml new file mode 100644 index 0000000000..eb7048521d --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/lint-baseline.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/embrace-bytecode-instrumentation-tests/src/main/AndroidManifest.xml b/embrace-bytecode-instrumentation-tests/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ActivityOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ActivityOnClickListener.java new file mode 100644 index 0000000000..746f20171c --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ActivityOnClickListener.java @@ -0,0 +1,15 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; + +/** + * An Activity which implements OnClickListener. + */ +public class ActivityOnClickListener extends AppCompatActivity implements View.OnClickListener { + @Override + public void onClick(View view) { + + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnClickListener.java new file mode 100644 index 0000000000..48b7ad87b4 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnClickListener.java @@ -0,0 +1,17 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An anonymous inner class which implements OnClickListener. + */ +public class AnonInnerClassOnClickListener { + public void setupListeners() { + new View.OnClickListener() { + @Override + public void onClick(View view) { + + } + }; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnLongClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnLongClickListener.java new file mode 100644 index 0000000000..f142d18956 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/AnonInnerClassOnLongClickListener.java @@ -0,0 +1,17 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An anonymous inner class which implements OnLongClickListener. + */ +public class AnonInnerClassOnLongClickListener { + public void setupListeners() { + new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + return false; + } + }; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ControlObject.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ControlObject.java new file mode 100644 index 0000000000..b603bd9ff0 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ControlObject.java @@ -0,0 +1,14 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; + +/** + * An object which should not be affected by bytecode instrumentation. + */ +public class ControlObject extends AppCompatActivity { + public void processView(View view) { + + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnClickListener.kt new file mode 100644 index 0000000000..0400a82a8f --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnClickListener.kt @@ -0,0 +1,14 @@ +package io.embrace.test.fixtures + +import android.view.View + +/** + * A custom object which implements OnClickListener. + */ +open class CustomOnClickListener : View.OnClickListener { + override fun onClick(view: View) { + if (view.isActivated) { + return + } + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnLongClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnLongClickListener.kt new file mode 100644 index 0000000000..62363ac2a4 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomOnLongClickListener.kt @@ -0,0 +1,13 @@ +package io.embrace.test.fixtures + +import android.view.View + +/** + * A custom object which implements OnLongClickListener. + */ +open class CustomOnLongClickListener : View.OnLongClickListener { + + override fun onLongClick(view: View?): Boolean { + return view?.isActivated ?: false + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomWebViewClient.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomWebViewClient.kt new file mode 100644 index 0000000000..c4f46c2fa6 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/CustomWebViewClient.kt @@ -0,0 +1,12 @@ +package io.embrace.test.fixtures + +import android.graphics.Bitmap +import android.webkit.WebView +import android.webkit.WebViewClient + +open class CustomWebViewClient : WebViewClient() { + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedCustomWebViewClient.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedCustomWebViewClient.kt new file mode 100644 index 0000000000..263c46b51d --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedCustomWebViewClient.kt @@ -0,0 +1,10 @@ +package io.embrace.test.fixtures + +import android.graphics.Bitmap +import android.webkit.WebView + +class ExtendedCustomWebViewClient : CustomWebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, "http://google.com", favicon) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnClickListener.java new file mode 100644 index 0000000000..d34c053b9c --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnClickListener.java @@ -0,0 +1,27 @@ +package io.embrace.test.fixtures; + +import android.util.Log; +import android.view.View; + +import org.jetbrains.annotations.NotNull; + +/** + * A class which extends and overrides an OnClickListener. + */ +public class ExtendedOnClickListener extends CustomOnClickListener { + + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + public void onClick(@NotNull View view) { + doSomething(); + super.onClick(view); + + if (view.isEnabled()) { + Log.d("EmbraceTest", "Clicked a button"); + } + } + + private int doSomething() { + return 5209 * 209; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnLongClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnLongClickListener.java new file mode 100644 index 0000000000..9c18138a71 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/ExtendedOnLongClickListener.java @@ -0,0 +1,26 @@ +package io.embrace.test.fixtures; + +import android.util.Log; +import android.view.View; + +import androidx.annotation.Nullable; + +import org.jetbrains.annotations.NotNull; + +/** + * A class which extends and overrides an OnLongClickListener. + */ +public class ExtendedOnLongClickListener extends CustomOnLongClickListener { + + @Override + public boolean onLongClick(@Nullable View view) { + if (view.isEnabled()) { + Log.d("EmbraceTest", "Clicked a button"); + } + return super.onLongClick(view); + } + + private int doSomething() { + return 5209 * 209; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/FragmentOnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/FragmentOnClickListener.kt new file mode 100644 index 0000000000..d498c024d0 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/FragmentOnClickListener.kt @@ -0,0 +1,12 @@ +package io.embrace.test.fixtures + +import android.view.View +import androidx.fragment.app.Fragment + +/** + * A Fragment which implements OnClickListener. + */ +class FragmentOnClickListener : Fragment(), View.OnClickListener { + override fun onClick(view: View) { + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaAnonOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaAnonOnClickListener.java new file mode 100644 index 0000000000..1e5d131721 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaAnonOnClickListener.java @@ -0,0 +1,31 @@ +package io.embrace.test.fixtures; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +/** + * A custom object which implements OnClickListener via a lambda. + */ +@SuppressWarnings("Convert2Lambda") +public class JavaAnonOnClickListener extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view != null) { + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View lambdaView) { + Log.d("Embrace", "test "); + } + }); + } + return view; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnClickListener.java new file mode 100644 index 0000000000..41278169b5 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnClickListener.java @@ -0,0 +1,27 @@ +package io.embrace.test.fixtures; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +/** + * A custom object which implements OnClickListener via a lambda. + */ +public class JavaLambdaOnClickListener extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view != null) { + view.setOnClickListener(lambdaView -> { + Log.d("Embrace", "test"); + }); + } + return view; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnLongClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnLongClickListener.java new file mode 100644 index 0000000000..46c0921df7 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaLambdaOnLongClickListener.java @@ -0,0 +1,28 @@ +package io.embrace.test.fixtures; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +/** + * A custom object which implements OnLongClickListener via a lambda. + */ +public class JavaLambdaOnLongClickListener extends Fragment { + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + if (view != null) { + view.setOnLongClickListener(lambdaView -> { + Log.d("Embrace", "test"); + return true; + }); + } + return view; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaNested.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaNested.java new file mode 100644 index 0000000000..bbfdea0e98 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/JavaNested.java @@ -0,0 +1,24 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * Contains nested classes which implement OnClickListener. + */ +public class JavaNested { + + @SuppressWarnings("InnerClassMayBeStatic") + public class JavaInnerListener implements View.OnClickListener { + @Override + public void onClick(View view) { + + } + } + + public static class JavaStaticListener implements View.OnClickListener { + @Override + public void onClick(View view) { + + } + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnClickListener.kt new file mode 100644 index 0000000000..2d61798feb --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnClickListener.kt @@ -0,0 +1,20 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnClickListener as a lambda. + */ +class KotlinLambdaOnClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnClickListener { + } + return view + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnLongClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnLongClickListener.kt new file mode 100644 index 0000000000..2b51c21014 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinLambdaOnLongClickListener.kt @@ -0,0 +1,21 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnLongClickListener as a lambda. + */ +class KotlinLambdaOnLongClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnLongClickListener { + false + } + return view + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRef2OnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRef2OnClickListener.kt new file mode 100644 index 0000000000..2cd76a657a --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRef2OnClickListener.kt @@ -0,0 +1,25 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnClickListener as an object. + */ +class KotlinMethodRef2OnClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnClickListener(this::performFoo) + return view + } + + @Suppress("UNUSED_PARAMETER") + public fun performFoo(v: View) { + Log.d("Embrace", "test") + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnClickListener.kt new file mode 100644 index 0000000000..75e6ebf69d --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnClickListener.kt @@ -0,0 +1,25 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnClickListener as an object. + */ +class KotlinMethodRefOnClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnClickListener(this::performFoo) + return view + } + + @Suppress("UNUSED_PARAMETER") + private fun performFoo(v: View) { + Log.d("Embrace", "test") + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener.kt new file mode 100644 index 0000000000..066c2821fd --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener.kt @@ -0,0 +1,26 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnLongClickListener as an object. + */ +class KotlinMethodRefOnLongClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnLongClickListener(this::performFoo) + return view + } + + @Suppress("UNUSED_PARAMETER") + private fun performFoo(v: View): Boolean { + Log.d("Embrace", "test") + return true + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNested.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNested.kt new file mode 100644 index 0000000000..d73210c5b7 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNested.kt @@ -0,0 +1,16 @@ +package io.embrace.test.fixtures + +import android.view.View + +/** + * Contains nested classes which implement OnClickListener. + */ +class KotlinNested { + inner class KotlinInnerListener : View.OnClickListener { + override fun onClick(view: View) {} + } + + class KotlinStaticListener : View.OnClickListener { + override fun onClick(view: View) {} + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNestedOnLongClick.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNestedOnLongClick.kt new file mode 100644 index 0000000000..8b782636c9 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinNestedOnLongClick.kt @@ -0,0 +1,20 @@ +package io.embrace.test.fixtures + +import android.view.View + +/** + * Contains nested classes which implement OnLongClickListener. + */ +class KotlinNestedOnLongClick { + inner class OnLongClickInnerListener : View.OnLongClickListener { + override fun onLongClick(view: View?): Boolean { + return true + } + } + + class OnLongClickStaticListener : View.OnLongClickListener { + override fun onLongClick(view: View?): Boolean { + return true + } + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinObjectOnClickListener.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinObjectOnClickListener.kt new file mode 100644 index 0000000000..763cbb3043 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/KotlinObjectOnClickListener.kt @@ -0,0 +1,23 @@ +package io.embrace.test.fixtures + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +/** + * A Kotlin class which implements an OnClickListener as an object. + */ +@Suppress("ObjectLiteralToLambda") +class KotlinObjectOnClickListener : Fragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = checkNotNull(super.onCreateView(inflater, container, savedInstanceState)) + view.setOnClickListener(object : View.OnClickListener { + override fun onClick(view: View?) { + } + }) + return view + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MethodReturnValueVisitorObj.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MethodReturnValueVisitorObj.kt new file mode 100644 index 0000000000..6bd946856a --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MethodReturnValueVisitorObj.kt @@ -0,0 +1,11 @@ +package io.embrace.test.fixtures + +@Suppress("FunctionOnlyReturningConstant") +class MethodReturnValueVisitorObj { + fun getSomeBool(): Boolean = false + fun getSomeInt(): Int = 100 + fun getSomeLong(): Long = 2 + fun getSomeStr(): String = "Hi" + fun getSomeList(): List = emptyList() + fun getSomeMap(): Map = emptyMap() +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnClickListener.java new file mode 100644 index 0000000000..744a7984d0 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnClickListener.java @@ -0,0 +1,15 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An object which has onClick methods but that does not implement OnClickListener. + */ +public class MissingInterfaceOnClickListener { + public void onClick(View view) { + + } + + public void onClick() { + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnLongClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnLongClickListener.java new file mode 100644 index 0000000000..85cd59ecba --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingInterfaceOnLongClickListener.java @@ -0,0 +1,16 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An object which has onLongClick methods but that does not implement OnLongClickListener. + */ +public class MissingInterfaceOnLongClickListener { + public boolean onLongClick(View view) { + return true; + } + + public boolean onLongClick() { + return true; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnClickListener.java new file mode 100644 index 0000000000..a9f9c9f784 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnClickListener.java @@ -0,0 +1,12 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An object which implements OnClickListener but forgot the override annotation. + */ +public class MissingOverrideOnClickListener implements View.OnClickListener { + public void onClick(View view) { + + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnLongClickListener.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnLongClickListener.java new file mode 100644 index 0000000000..b3d5a41056 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/MissingOverrideOnLongClickListener.java @@ -0,0 +1,12 @@ +package io.embrace.test.fixtures; + +import android.view.View; + +/** + * An object which implements OnLongClickListener but forgot the override annotation. + */ +public class MissingOverrideOnLongClickListener implements View.OnLongClickListener { + public boolean onLongClick(View view) { + return false; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/NoOverrideWebViewClient.kt b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/NoOverrideWebViewClient.kt new file mode 100644 index 0000000000..d560989651 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/NoOverrideWebViewClient.kt @@ -0,0 +1,8 @@ +package io.embrace.test.fixtures + +import android.webkit.WebViewClient + +/** + * A WebViewClient that does not override onPageStarted. + */ +class NoOverrideWebViewClient : WebViewClient() diff --git a/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/VirtualMethodRefNamedOnClick.java b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/VirtualMethodRefNamedOnClick.java new file mode 100644 index 0000000000..e903c2a650 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/main/java/io/embrace/test/fixtures/VirtualMethodRefNamedOnClick.java @@ -0,0 +1,30 @@ +package io.embrace.test.fixtures; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import java.util.Objects; + +/** + * A custom object which implements OnClickListener via a _virtual_ method reference + * that is called onClick + */ +public class VirtualMethodRefNamedOnClick extends Fragment { + + private void onClick(View lambdaView) { + Log.d("Embrace", "test"); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + Objects.requireNonNull(view).setOnClickListener(this::onClick); + return view; + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/config/ClassInstrumentationFilterTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/config/ClassInstrumentationFilterTest.kt new file mode 100644 index 0000000000..c4ffe07891 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/config/ClassInstrumentationFilterTest.kt @@ -0,0 +1,102 @@ +package io.embrace.gradle.plugin.config + +import io.embrace.android.gradle.plugin.instrumentation.ClassInstrumentationFilter +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ClassInstrumentationFilterTest { + + @Test + fun `it should not skip if there are no skip pattern for jar`() { + val filter = ClassInstrumentationFilter(emptyList()) + assertFalse(filter.shouldSkip("whatever")) + } + + @Test + fun `it should not skip if there are no skip pattern for class`() { + val filter = ClassInstrumentationFilter(emptyList()) + assertFalse(filter.shouldSkip("whatever")) + } + + @Test + fun `it should not skip if name does not match any pattern for jar`() { + val filter = ClassInstrumentationFilter(listOf("jarToSkip")) + assertFalse(filter.shouldSkip("jarNotToSkip")) + } + + @Test + fun `it should not skip if name does not match any pattern for class`() { + val filter = ClassInstrumentationFilter(listOf("com.myclass.toSkip")) + assertFalse(filter.shouldSkip("com.myclass.notToSkip")) + } + + @Test + fun `it should skip if name matches a pattern for jar`() { + val filter = ClassInstrumentationFilter(listOf("com.myclass.toSkip")) + assertTrue(filter.shouldSkip("com.myclass.toSkip")) + } + + @Test + fun `it should skip if name matches a pattern for class`() { + val filter = ClassInstrumentationFilter(listOf("com.myclass.toSkip")) + assertTrue(filter.shouldSkip("com.myclass.toSkip")) + } + + @Test + fun skipClassWhenRegexAffectsClassName() { + // given + val filter = ClassInstrumentationFilter(listOf("Hel.*")) + + // when + val shouldSkipInstrumentation = filter.shouldSkip("HelloFragment.class") + + // then + assertTrue(shouldSkipInstrumentation) + } + + @Test + fun skipClassWhenRegexAffectsJarName() { + // given + val filter = ClassInstrumentationFilter(listOf("Hel.*")) + + // when + val shouldSkipInstrumentation = + filter.shouldSkip("Hello-Library.jar") + + // then + assertTrue(shouldSkipInstrumentation) + } + + @Test + fun skipMultipleJars() { + // given + val filter = ClassInstrumentationFilter(listOf("Hello", "Go.*bye", "Hey")) + + // when + val skipContainingWord = filter.shouldSkip("Hello-Library.jar") + val skipRegex = filter.shouldSkip("Goodbye.jar") + val doesNotSkip = filter.shouldSkip("Another.jar") + + // then + assertTrue(skipContainingWord) + assertTrue(skipRegex) + assertFalse(doesNotSkip) + } + + @Test + fun skipMultipleClasses() { + // given + val filter = ClassInstrumentationFilter(listOf("Hello", "Go.*bye", "Hey")) + + // when + val skipContainingWord = filter.shouldSkip("HelloFragment.class") + val skipRegex = filter.shouldSkip("GoodbyeActivity.class") + val doesNotSkip = filter.shouldSkip("AnotherClass.class") + + // then + assertTrue(skipContainingWord) + assertTrue(skipRegex) + assertFalse(doesNotSkip) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/BytecodeTestParams.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/BytecodeTestParams.kt new file mode 100644 index 0000000000..dadf7323af --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/BytecodeTestParams.kt @@ -0,0 +1,52 @@ +package io.embrace.gradle.plugin.instrumentation + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import kotlin.reflect.KClass + +// constants checked against AGP 7.4.2 + +// com.android.build.gradle.internal.instrumentation.InstrumentationUtils.ASM_API_VERSION_FOR_INSTRUMENTATION +internal const val ASM_API_VERSION = Opcodes.ASM9 + +// com.android.build.gradle.internal.instrumentation.AsmInstrumentationManager#getClassReaderFlags +internal const val ASM_CLASS_READER_FLAGS = ClassReader.EXPAND_FRAMES + +// com.android.build.gradle.internal.instrumentation.AsmInstrumentationManager#getClassWriterFlags +internal const val ASM_CLASS_WRITER_FLAGS = ClassWriter.COMPUTE_FRAMES + +internal typealias ClassVisitorFactory = (nextVisitor: ClassVisitor) -> ClassVisitor + +/** + * Test parameters used to instrument bytecode. + */ +class BytecodeTestParams( + clz: Class<*>, + val qualifiedClzName: String = clz.name, + val simpleClzName: String = clz.simpleName, + val expectedOutput: String = "${simpleClzName}_expected.txt", + val factory: ClassVisitorFactory = { nextVisitor -> + nextVisitor + } +) { + + companion object { + fun forInnerClass( + kClass: KClass<*>, + innerClzName: String, + factory: ClassVisitorFactory = { nextVisitor -> + nextVisitor + } + ): BytecodeTestParams { + return BytecodeTestParams( + kClass.java, + qualifiedClzName = "${kClass.java.name}$innerClzName", + factory = factory + ) + } + } + + override fun toString() = "$simpleClzName => $expectedOutput" +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassContext.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassContext.kt new file mode 100644 index 0000000000..53f8337769 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassContext.kt @@ -0,0 +1,15 @@ +package io.embrace.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData + +class FakeClassContext( + override val currentClassData: ClassData +) : ClassContext { + + constructor(clzName: String) : this(FakeClassData(clzName)) + + override fun loadClassData(className: String): ClassData? { + error("Unsupported operation") + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassData.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassData.kt new file mode 100644 index 0000000000..83074b2c73 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/FakeClassData.kt @@ -0,0 +1,10 @@ +package io.embrace.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.ClassData + +class FakeClassData( + override val className: String, + override val classAnnotations: List = emptyList(), + override val interfaces: List = emptyList(), + override val superClasses: List = emptyList(), +) : ClassData diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentationRunner.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentationRunner.kt new file mode 100644 index 0000000000..1ff7328d9e --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentationRunner.kt @@ -0,0 +1,90 @@ +package io.embrace.gradle.plugin.instrumentation + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.util.CheckClassAdapter +import org.objectweb.asm.util.TraceClassVisitor +import java.io.PrintWriter +import java.io.StringWriter + +private val REGEX_TRAILING_WHITESPACE = "[ ]*\n".toRegex() +private val REGEX_PACKAGE_NAME = "embrace-bytecode-instrumentation-tests_(debug|release)".toRegex() +private const val REPLACEMENT_PACKAGE_NAME = "embrace-bytecode-instrumentation-tests_release" + +object InstrumentationRunner { + + /** + * Loads a class & runs instrumentation against it, returning a string representation of + * the bytecode output. This output is then compared against a known valid output & the test + * is failed if it differs. + */ + fun runInstrumentationAndCompareOutput(params: BytecodeTestParams) { + with(params) { + val output = runInstrumentation(qualifiedClzName, factory) + assertEquals( + "The bytecode representation has changed from the known valid " + + "output. Please confirm whether this is intentional by comparing the " + + "input/output generated via TraceClassVisitor.", + loadResourceAsText(expectedOutput), + sanitizeOutput(output) + ) + } + } + + /** + * Loads a class & runs instrumentation against it, returning a string representation of + * the bytecode output. + */ + private fun runInstrumentation( + fqClzName: String, + factory: ClassVisitorFactory + ): String { + val reader = ClassReader(fqClzName) + val writer = ClassWriter(reader, ASM_CLASS_WRITER_FLAGS) + + // visit the class with a TraceClassVisitor which prints a textual representation + // of the generated bytecode. + val stringWriter = StringWriter() + val traceVisitor = TraceClassVisitor(writer, PrintWriter(stringWriter)) + reader.accept(factory(traceVisitor), ASM_CLASS_READER_FLAGS) + + // perform a sanity check that the bytecode is well-formed + val bytecode = writer.toByteArray() + sanityCheckBytecode(bytecode) + + // return the output of the TraceClassVisitor + return stringWriter.toString() + } + + /** + * Loads a resource and reads it as a [String]. + */ + fun loadResourceAsText(resName: String): String { + val classLoader = checkNotNull(javaClass.classLoader) + val res = classLoader.getResourceAsStream(resName) + ?: error("Could not find expected fixture resource named '$resName'") + return res.bufferedReader().use { it.readText() } + } + + /** + * Removes trailing whitespace from the output, and normalize the package name + * (which is set in Kotlin Metadata). + */ + private fun sanitizeOutput(output: String): String { + return output.replace(REGEX_TRAILING_WHITESPACE, "\n") + .replace(REGEX_PACKAGE_NAME, REPLACEMENT_PACKAGE_NAME) + } + + /** + * Uses a [CheckClassAdapter] which sanity checks that the generated bytecode is well formed. + */ + private fun sanityCheckBytecode(bytes: ByteArray) { + val reader = ClassReader(bytes) + val writer = ClassWriter(reader, ASM_CLASS_WRITER_FLAGS) + val adapter = CheckClassAdapter(writer, true) + reader.accept(adapter, ASM_CLASS_READER_FLAGS) + assertArrayEquals(bytes, writer.toByteArray()) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTest.kt new file mode 100644 index 0000000000..e7f1c3fe05 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTest.kt @@ -0,0 +1,47 @@ +package io.embrace.gradle.plugin.instrumentation + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.util.TraceClassVisitor + +/** + * Verifies that a [ClassVisitor] produces the correct bytecode output for a given class. + * + * For example, if a class implements [View.OnClickListener] then the embrace gradle plugin should + * instrument the bytecode so that the first line of the onClick method contains a call to + * [ViewSwazzledHooks._preOnClick]. If a class does not implement the interface, then its + * bytecode should remain the same. + * + * The test achieves this verification using the following approach: + * + * 1. Define a class and load it via the default ClassLoader + * 2. Create a [ClassReader] instance that reads the class in WebObject ASM + * 3. Process the class using the [ClassVisitor] instance that instruments the bytecode + * 4. Compare the bytecode representation obtained from a [TraceClassVisitor] with a known output + * + * To add more test cases please use [instrumentedBytecodeTestCases] and read the README. + * + * For more information on WebObject ASM, see https://asm.ow2.io/ + */ +@RunWith(Parameterized::class) +class InstrumentedBytecodeTest( + private val params: BytecodeTestParams +) { + + /** + * To add more test cases please use [instrumentedBytecodeTestCases] and read the README. + */ + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: {0}") + fun testCases() = instrumentedBytecodeTestCases() + } + + @Test + fun testInstrumentedBytecode() { + InstrumentationRunner.runInstrumentationAndCompareOutput(params) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTestCases.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTestCases.kt new file mode 100644 index 0000000000..029944cdd3 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/InstrumentedBytecodeTestCases.kt @@ -0,0 +1,134 @@ +package io.embrace.gradle.plugin.instrumentation + +import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnLongClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.WebViewClientClassAdapter +import io.embrace.test.fixtures.ActivityOnClickListener +import io.embrace.test.fixtures.AnonInnerClassOnClickListener +import io.embrace.test.fixtures.AnonInnerClassOnLongClickListener +import io.embrace.test.fixtures.ControlObject +import io.embrace.test.fixtures.CustomOnClickListener +import io.embrace.test.fixtures.CustomOnLongClickListener +import io.embrace.test.fixtures.CustomWebViewClient +import io.embrace.test.fixtures.ExtendedCustomWebViewClient +import io.embrace.test.fixtures.ExtendedOnClickListener +import io.embrace.test.fixtures.ExtendedOnLongClickListener +import io.embrace.test.fixtures.FragmentOnClickListener +import io.embrace.test.fixtures.JavaAnonOnClickListener +import io.embrace.test.fixtures.JavaLambdaOnClickListener +import io.embrace.test.fixtures.JavaLambdaOnLongClickListener +import io.embrace.test.fixtures.JavaNested +import io.embrace.test.fixtures.KotlinNested +import io.embrace.test.fixtures.KotlinNestedOnLongClick +import io.embrace.test.fixtures.KotlinObjectOnClickListener +import io.embrace.test.fixtures.MissingInterfaceOnClickListener +import io.embrace.test.fixtures.MissingInterfaceOnLongClickListener +import io.embrace.test.fixtures.MissingOverrideOnClickListener +import io.embrace.test.fixtures.MissingOverrideOnLongClickListener +import io.embrace.test.fixtures.NoOverrideWebViewClient +import io.embrace.test.fixtures.VirtualMethodRefNamedOnClick +import okhttp3.OkHttpClient +import org.objectweb.asm.ClassVisitor + +private val onClickFactory: ClassVisitorFactory = { visitor -> + OnClickClassAdapter(ASM_API_VERSION, visitor) {} +} + +private val onLongClickFactory: ClassVisitorFactory = { visitor -> + OnLongClickClassAdapter(ASM_API_VERSION, visitor) {} +} + +private val webviewFactory: ClassVisitorFactory = { visitor -> + WebViewClientClassAdapter(ASM_API_VERSION, visitor) {} +} + +private val okHttpFactory: ClassVisitorFactory = { visitor -> + OkHttpClassAdapter(ASM_API_VERSION, visitor) {} +} + +/** + * Declares the test cases for bytecode in [InstrumentedBytecodeTest]. You should define the + * input class, the expected output, and the [ClassVisitor] which will instrument the bytecode. + * + * After doing that, [InstrumentedBytecodeTest] will perform all the necessary checks automatically! + */ +internal fun instrumentedBytecodeTestCases(): List { + return onClickTestCases + .plus(onClickInnerTestCases) + .plus(onLongClickTestCases) + .plus(onLongClickInnerTestCases) + .plus(webclientTestCases) + .plus(okHttpTestCases) + .distinct() // filter out any unintentional duplicate test cases + .sortedBy(BytecodeTestParams::simpleClzName) +} + +private val okHttpTestCases = listOf( + OkHttpClient.Builder::class +).map { + BytecodeTestParams(it.java, factory = okHttpFactory) +} +private val webclientTestCases = listOf( + CustomWebViewClient::class, + ExtendedCustomWebViewClient::class, + NoOverrideWebViewClient::class +).map { + BytecodeTestParams(it.java, factory = webviewFactory) +} + +private val onClickTestCases = listOf( + ActivityOnClickListener::class, + ControlObject::class, + CustomOnClickListener::class, + ExtendedOnClickListener::class, + FragmentOnClickListener::class, + JavaNested.JavaInnerListener::class, + JavaNested.JavaStaticListener::class, + KotlinNested.KotlinInnerListener::class, + KotlinNested.KotlinStaticListener::class, + MissingInterfaceOnClickListener::class, + MissingOverrideOnClickListener::class, + VirtualMethodRefNamedOnClick::class, + JavaLambdaOnClickListener::class, +).map { clz -> + BytecodeTestParams(clz.java, factory = onClickFactory) +} + +private val onClickInnerTestCases = listOf( + BytecodeTestParams.forInnerClass( + AnonInnerClassOnClickListener::class, + "$1", + factory = onClickFactory + ), + BytecodeTestParams.forInnerClass( + JavaAnonOnClickListener::class, + "$1", + factory = onClickFactory + ), + BytecodeTestParams.forInnerClass( + KotlinObjectOnClickListener::class, + "\$onCreateView$1", + factory = onClickFactory + ) +) + +private val onLongClickTestCases = listOf( + CustomOnLongClickListener::class, + ExtendedOnLongClickListener::class, + JavaLambdaOnLongClickListener::class, + KotlinNestedOnLongClick.OnLongClickInnerListener::class, + KotlinNestedOnLongClick.OnLongClickStaticListener::class, + MissingInterfaceOnLongClickListener::class, + MissingOverrideOnLongClickListener::class, +).map { clz -> + BytecodeTestParams(clz.java, factory = onLongClickFactory) +} + +private val onLongClickInnerTestCases = listOf( + BytecodeTestParams.forInnerClass( + AnonInnerClassOnLongClickListener::class, + "$1", + factory = onLongClickFactory + ) +) diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/MethodReturnValueVisitorTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/MethodReturnValueVisitorTest.kt new file mode 100644 index 0000000000..fbba8eab03 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/MethodReturnValueVisitorTest.kt @@ -0,0 +1,92 @@ +package io.embrace.gradle.plugin.instrumentation + +import io.embrace.android.gradle.plugin.instrumentation.config.BooleanReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.IntReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.LongReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.MapReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringReturnValueMethodVisitor +import io.embrace.test.fixtures.MethodReturnValueVisitorObj +import org.junit.Test +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +class MethodReturnValueVisitorTest { + + companion object { + private const val BOOL_METHOD_NAME = "getSomeBool" + private const val BOOL_METHOD_DESCRIPTOR = "()Z" + private const val LONG_METHOD_NAME = "getSomeLong" + private const val LONG_METHOD_DESCRIPTOR = "()J" + private const val INT_METHOD_NAME = "getSomeInt" + private const val INT_METHOD_DESCRIPTOR = "()I" + private const val STRING_METHOD_NAME = "getSomeStr" + private const val STRING_METHOD_DESCRIPTOR = "()Ljava/lang/String;" + private const val LIST_METHOD_NAME = "getSomeList" + private const val LIST_METHOD_DESCRIPTOR = "()Ljava/util/List;" + private const val MAP_METHOD_NAME = "getSomeMap" + private const val MAP_METHOD_DESCRIPTOR = "()Ljava/util/Map;" + } + + @Test + fun `instrument bytecode`() { + val params = BytecodeTestParams(MethodReturnValueVisitorObj::class.java) { nextVisitor -> + TestClassVisitor(ASM_API_VERSION, nextVisitor) + } + InstrumentationRunner.runInstrumentationAndCompareOutput(params) + } + + /** + * Visits the [MethodReturnValueVisitorObj] class and alters the return values for each function. + */ + class TestClassVisitor( + api: Int, + nextClassVisitor: ClassVisitor? + ) : ClassVisitor(api, nextClassVisitor) { + + override fun visitMethod( + access: Int, + name: String?, + descriptor: String?, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val nextVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) + + if (name == BOOL_METHOD_NAME && descriptor == BOOL_METHOD_DESCRIPTOR) { + return BooleanReturnValueMethodVisitor( + true, + api, + nextVisitor + ) + } else if (name == LONG_METHOD_NAME && descriptor == LONG_METHOD_DESCRIPTOR) { + return LongReturnValueMethodVisitor( + 150900202020202L, + api, + nextVisitor + ) + } else if (name == INT_METHOD_NAME && descriptor == INT_METHOD_DESCRIPTOR) { + return IntReturnValueMethodVisitor( + 520, + api, + nextVisitor + ) + } else if (name == STRING_METHOD_NAME && descriptor == STRING_METHOD_DESCRIPTOR) { + return StringReturnValueMethodVisitor("Hello world! I'm a string.", api, nextVisitor) + } else if (name == LIST_METHOD_NAME && descriptor == LIST_METHOD_DESCRIPTOR) { + return io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor( + listOf("adam aardvark", "bob banana"), + api, + nextVisitor + ) + } else if (name == MAP_METHOD_NAME && descriptor == MAP_METHOD_DESCRIPTOR) { + return MapReturnValueMethodVisitor( + mapOf("adam" to "1", "bob" to "2"), + api, + nextVisitor + ) + } + return nextVisitor + } + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OkHttpClassFilterTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OkHttpClassFilterTest.kt new file mode 100644 index 0000000000..1d7393a5c5 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OkHttpClassFilterTest.kt @@ -0,0 +1,21 @@ +package io.embrace.gradle.plugin.instrumentation + +import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class OkHttpClassFilterTest { + + @Test + fun testClassAccepted() { + val ctx = FakeClassContext("okhttp3.OkHttpClient\$Builder") + assertTrue(OkHttpClassAdapter.accept(ctx)) + } + + @Test + fun testClassNotAccepted() { + val ctx = FakeClassContext("okhttp3.OkHttpClient") + assertFalse(OkHttpClassAdapter.accept(ctx)) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OnClickClassFilterTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OnClickClassFilterTest.kt new file mode 100644 index 0000000000..de82b2d820 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/OnClickClassFilterTest.kt @@ -0,0 +1,14 @@ +package io.embrace.gradle.plugin.instrumentation + +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter +import org.junit.Assert.assertTrue +import org.junit.Test + +class OnClickClassFilterTest { + + @Test + fun testClassAccepted() { + val ctx = FakeClassContext("org/test/FooBar") + assertTrue(OnClickClassAdapter.accept(ctx)) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/WebViewClassFilterTest.kt b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/WebViewClassFilterTest.kt new file mode 100644 index 0000000000..56e999e512 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/java/io/embrace/gradle/plugin/instrumentation/WebViewClassFilterTest.kt @@ -0,0 +1,31 @@ +package io.embrace.gradle.plugin.instrumentation + +import io.embrace.android.gradle.plugin.instrumentation.visitor.WebViewClientClassAdapter +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class WebViewClassFilterTest { + + @Test + fun testClassAccepted() { + val ctx = FakeClassContext( + FakeClassData( + "", + superClasses = listOf("android.webkit.WebViewClient") + ) + ) + assertTrue(WebViewClientClassAdapter.accept(ctx)) + } + + @Test + fun testClassNotAccepted() { + val ctx = FakeClassContext( + FakeClassData( + "", + superClasses = emptyList() + ) + ) + assertFalse(WebViewClientClassAdapter.accept(ctx)) + } +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/ActivityOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/ActivityOnClickListener_expected.txt new file mode 100644 index 0000000000..960ea3da79 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/ActivityOnClickListener_expected.txt @@ -0,0 +1,34 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/ActivityOnClickListener extends androidx/appcompat/app/AppCompatActivity implements android/view/View$OnClickListener { + + // compiled from: ActivityOnClickListener.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 10 L0 + ALOAD 0 + INVOKESPECIAL androidx/appcompat/app/AppCompatActivity. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ActivityOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 14 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ActivityOnClickListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnClickListener_expected.txt new file mode 100644 index 0000000000..96c8748be1 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnClickListener_expected.txt @@ -0,0 +1,45 @@ +// class version 55.0 (55) +// access flags 0x20 +class io/embrace/test/fixtures/AnonInnerClassOnClickListener$1 implements android/view/View$OnClickListener { + + // compiled from: AnonInnerClassOnClickListener.java + NESTHOST io/embrace/test/fixtures/AnonInnerClassOnClickListener + OUTERCLASS io/embrace/test/fixtures/AnonInnerClassOnClickListener setupListeners ()V + // access flags 0x0 + INNERCLASS io/embrace/test/fixtures/AnonInnerClassOnClickListener$1 null null + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/AnonInnerClassOnClickListener; this$0 + + // access flags 0x0 + (Lio/embrace/test/fixtures/AnonInnerClassOnClickListener;)V + L0 + LINENUMBER 10 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/AnonInnerClassOnClickListener$1.this$0 : Lio/embrace/test/fixtures/AnonInnerClassOnClickListener; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/AnonInnerClassOnClickListener$1; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/AnonInnerClassOnClickListener; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 14 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/AnonInnerClassOnClickListener$1; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnLongClickListener_expected.txt new file mode 100644 index 0000000000..679e21da83 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/AnonInnerClassOnLongClickListener_expected.txt @@ -0,0 +1,46 @@ +// class version 55.0 (55) +// access flags 0x20 +class io/embrace/test/fixtures/AnonInnerClassOnLongClickListener$1 implements android/view/View$OnLongClickListener { + + // compiled from: AnonInnerClassOnLongClickListener.java + NESTHOST io/embrace/test/fixtures/AnonInnerClassOnLongClickListener + OUTERCLASS io/embrace/test/fixtures/AnonInnerClassOnLongClickListener setupListeners ()V + // access flags 0x0 + INNERCLASS io/embrace/test/fixtures/AnonInnerClassOnLongClickListener$1 null null + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener; this$0 + + // access flags 0x0 + (Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener;)V + L0 + LINENUMBER 10 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/AnonInnerClassOnLongClickListener$1.this$0 : Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener$1; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 13 L0 + ICONST_0 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/AnonInnerClassOnLongClickListener$1; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/Builder_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/Builder_expected.txt new file mode 100644 index 0000000000..dbe58cf7f2 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/Builder_expected.txt @@ -0,0 +1,3395 @@ +// class version 52.0 (52) +// access flags 0x31 +public final class okhttp3/OkHttpClient$Builder { + + // compiled from: OkHttpClient.kt + // debug info: SMAP +OkHttpClient.kt +Kotlin +*S Kotlin +*F ++ 1 OkHttpClient.kt +okhttp3/OkHttpClient$Builder ++ 2 fake.kt +kotlin/jvm/internal/FakeKt +*L +1#1,1079:1 +1#2:1080 +*E + + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u00f8\u0001\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\u0008\u0003\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0010\u0008\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0008\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0010 \n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0010\u000b\n\u0002\u0008\u0008\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0010!\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0009\n\u0002\u0008\n\n\u0002\u0018\u0002\n\u0002\u0008\u0003\n\u0002\u0018\u0002\n\u0002\u0008\u0008\n\u0002\u0018\u0002\n\u0002\u0008\u000b\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0005\n\u0002\u0018\u0002\n\u0002\u0008\u0008\n\u0002\u0018\u0002\n\u0002\u0008\u0006\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0006\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0008\u0006\u0018\u00002\u00020\u0001B\u000f\u0008\u0010\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u00a2\u0006\u0002\u0010\u0004B\u0005\u00a2\u0006\u0002\u0010\u0005J?\u0010\u009e\u0001\u001a\u00020\u00002*\u0008\u0004\u0010\u009f\u0001\u001a#\u0012\u0017\u0012\u00150\u00a1\u0001\u00a2\u0006\u000f\u0008\u00a2\u0001\u0012\n\u0008\u00a3\u0001\u0012\u0005\u0008\u0008(\u00a4\u0001\u0012\u0005\u0012\u00030\u00a5\u00010\u00a0\u0001H\u0087\u0008\u00f8\u0001\u0000\u00a2\u0006\u0003\u0008\u00a6\u0001J\u0010\u0010\u009e\u0001\u001a\u00020\u00002\u0007\u0010\u00a7\u0001\u001a\u00020]J?\u0010\u00a8\u0001\u001a\u00020\u00002*\u0008\u0004\u0010\u009f\u0001\u001a#\u0012\u0017\u0012\u00150\u00a1\u0001\u00a2\u0006\u000f\u0008\u00a2\u0001\u0012\n\u0008\u00a3\u0001\u0012\u0005\u0008\u0008(\u00a4\u0001\u0012\u0005\u0012\u00030\u00a5\u00010\u00a0\u0001H\u0087\u0008\u00f8\u0001\u0000\u00a2\u0006\u0003\u0008\u00a9\u0001J\u0010\u0010\u00a8\u0001\u001a\u00020\u00002\u0007\u0010\u00a7\u0001\u001a\u00020]J\u000e\u0010\u0006\u001a\u00020\u00002\u0006\u0010\u0006\u001a\u00020\u0007J\u0007\u0010\u00aa\u0001\u001a\u00020\u0003J\u0010\u0010\u000c\u001a\u00020\u00002\u0008\u0010\u000c\u001a\u0004\u0018\u00010\rJ\u0012\u0010\u0012\u001a\u00020\u00002\u0008\u0010\u00ab\u0001\u001a\u00030\u00ac\u0001H\u0007J\u0019\u0010\u0012\u001a\u00020\u00002\u0007\u0010\u00ad\u0001\u001a\u00020`2\u0008\u0010\u00ae\u0001\u001a\u00030\u00af\u0001J\u000e\u0010\u001e\u001a\u00020\u00002\u0006\u0010\u001e\u001a\u00020\u001fJ\u0012\u0010$\u001a\u00020\u00002\u0008\u0010\u00ab\u0001\u001a\u00030\u00ac\u0001H\u0007J\u0019\u0010$\u001a\u00020\u00002\u0007\u0010\u00ad\u0001\u001a\u00020`2\u0008\u0010\u00ae\u0001\u001a\u00030\u00af\u0001J\u000e\u0010'\u001a\u00020\u00002\u0006\u0010'\u001a\u00020(J\u0014\u0010-\u001a\u00020\u00002\u000c\u0010-\u001a\u0008\u0012\u0004\u0012\u00020/0.J\u000e\u00104\u001a\u00020\u00002\u0006\u00104\u001a\u000205J\u000e\u0010:\u001a\u00020\u00002\u0006\u0010:\u001a\u00020;J\u000e\u0010@\u001a\u00020\u00002\u0006\u0010@\u001a\u00020AJ\u0011\u0010\u00b0\u0001\u001a\u00020\u00002\u0008\u0010\u00b0\u0001\u001a\u00030\u00b1\u0001J\u000e\u0010F\u001a\u00020\u00002\u0006\u0010F\u001a\u00020GJ\u000e\u0010L\u001a\u00020\u00002\u0006\u0010L\u001a\u00020MJ\u000f\u0010R\u001a\u00020\u00002\u0007\u0010\u00b2\u0001\u001a\u00020MJ\u000e\u0010U\u001a\u00020\u00002\u0006\u0010U\u001a\u00020VJ\u000c\u0010[\u001a\u0008\u0012\u0004\u0012\u00020]0\\J\u000f\u0010_\u001a\u00020\u00002\u0007\u0010\u00b3\u0001\u001a\u00020`J\u000c\u0010e\u001a\u0008\u0012\u0004\u0012\u00020]0\\J\u0012\u0010g\u001a\u00020\u00002\u0008\u0010\u00ab\u0001\u001a\u00030\u00ac\u0001H\u0007J\u0019\u0010g\u001a\u00020\u00002\u0007\u0010\u00b4\u0001\u001a\u00020`2\u0008\u0010\u00ae\u0001\u001a\u00030\u00af\u0001J\u0014\u0010j\u001a\u00020\u00002\u000c\u0010j\u001a\u0008\u0012\u0004\u0012\u00020k0.J\u0010\u0010n\u001a\u00020\u00002\u0008\u0010n\u001a\u0004\u0018\u00010oJ\u000e\u0010t\u001a\u00020\u00002\u0006\u0010t\u001a\u00020\u0007J\u000e\u0010w\u001a\u00020\u00002\u0006\u0010w\u001a\u00020xJ\u0012\u0010}\u001a\u00020\u00002\u0008\u0010\u00ab\u0001\u001a\u00030\u00ac\u0001H\u0007J\u0019\u0010}\u001a\u00020\u00002\u0007\u0010\u00ad\u0001\u001a\u00020`2\u0008\u0010\u00ae\u0001\u001a\u00030\u00af\u0001J\u0010\u0010\u0080\u0001\u001a\u00020\u00002\u0007\u0010\u0080\u0001\u001a\u00020MJ\u0011\u0010\u0089\u0001\u001a\u00020\u00002\u0008\u0010\u0089\u0001\u001a\u00030\u008a\u0001J\u0013\u0010\u00b5\u0001\u001a\u00020\u00002\u0008\u0010\u00b5\u0001\u001a\u00030\u0090\u0001H\u0007J\u001b\u0010\u00b5\u0001\u001a\u00020\u00002\u0008\u0010\u00b5\u0001\u001a\u00030\u0090\u00012\u0008\u0010\u00b6\u0001\u001a\u00030\u0099\u0001J\u0013\u0010\u0095\u0001\u001a\u00020\u00002\u0008\u0010\u00ab\u0001\u001a\u00030\u00ac\u0001H\u0007J\u001a\u0010\u0095\u0001\u001a\u00020\u00002\u0007\u0010\u00ad\u0001\u001a\u00020`2\u0008\u0010\u00ae\u0001\u001a\u00030\u00af\u0001R\u001a\u0010\u0006\u001a\u00020\u0007X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008\u0008\u0010\u0009\"\u0004\u0008\n\u0010\u000bR\u001c\u0010\u000c\u001a\u0004\u0018\u00010\rX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008\u000e\u0010\u000f\"\u0004\u0008\u0010\u0010\u0011R\u001a\u0010\u0012\u001a\u00020\u0013X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008\u0014\u0010\u0015\"\u0004\u0008\u0016\u0010\u0017R\u001c\u0010\u0018\u001a\u0004\u0018\u00010\u0019X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008\u001a\u0010\u001b\"\u0004\u0008\u001c\u0010\u001dR\u001a\u0010\u001e\u001a\u00020\u001fX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008 \u0010!\"\u0004\u0008\"\u0010#R\u001a\u0010$\u001a\u00020\u0013X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008%\u0010\u0015\"\u0004\u0008&\u0010\u0017R\u001a\u0010'\u001a\u00020(X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008)\u0010*\"\u0004\u0008+\u0010,R \u0010-\u001a\u0008\u0012\u0004\u0012\u00020/0.X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u00080\u00101\"\u0004\u00082\u00103R\u001a\u00104\u001a\u000205X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u00086\u00107\"\u0004\u00088\u00109R\u001a\u0010:\u001a\u00020;X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008<\u0010=\"\u0004\u0008>\u0010?R\u001a\u0010@\u001a\u00020AX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008B\u0010C\"\u0004\u0008D\u0010ER\u001a\u0010F\u001a\u00020GX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008H\u0010I\"\u0004\u0008J\u0010KR\u001a\u0010L\u001a\u00020MX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008N\u0010O\"\u0004\u0008P\u0010QR\u001a\u0010R\u001a\u00020MX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008S\u0010O\"\u0004\u0008T\u0010QR\u001a\u0010U\u001a\u00020VX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008W\u0010X\"\u0004\u0008Y\u0010ZR\u001a\u0010[\u001a\u0008\u0012\u0004\u0012\u00020]0\\X\u0080\u0004\u00a2\u0006\u0008\n\u0000\u001a\u0004\u0008^\u00101R\u001a\u0010_\u001a\u00020`X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008a\u0010b\"\u0004\u0008c\u0010dR\u001a\u0010e\u001a\u0008\u0012\u0004\u0012\u00020]0\\X\u0080\u0004\u00a2\u0006\u0008\n\u0000\u001a\u0004\u0008f\u00101R\u001a\u0010g\u001a\u00020\u0013X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008h\u0010\u0015\"\u0004\u0008i\u0010\u0017R \u0010j\u001a\u0008\u0012\u0004\u0012\u00020k0.X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008l\u00101\"\u0004\u0008m\u00103R\u001c\u0010n\u001a\u0004\u0018\u00010oX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008p\u0010q\"\u0004\u0008r\u0010sR\u001a\u0010t\u001a\u00020\u0007X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008u\u0010\u0009\"\u0004\u0008v\u0010\u000bR\u001c\u0010w\u001a\u0004\u0018\u00010xX\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008y\u0010z\"\u0004\u0008{\u0010|R\u001a\u0010}\u001a\u00020\u0013X\u0080\u000e\u00a2\u0006\u000e\n\u0000\u001a\u0004\u0008~\u0010\u0015\"\u0004\u0008\u0010\u0017R\u001d\u0010\u0080\u0001\u001a\u00020MX\u0080\u000e\u00a2\u0006\u0010\n\u0000\u001a\u0005\u0008\u0081\u0001\u0010O\"\u0005\u0008\u0082\u0001\u0010QR\"\u0010\u0083\u0001\u001a\u0005\u0018\u00010\u0084\u0001X\u0080\u000e\u00a2\u0006\u0012\n\u0000\u001a\u0006\u0008\u0085\u0001\u0010\u0086\u0001\"\u0006\u0008\u0087\u0001\u0010\u0088\u0001R \u0010\u0089\u0001\u001a\u00030\u008a\u0001X\u0080\u000e\u00a2\u0006\u0012\n\u0000\u001a\u0006\u0008\u008b\u0001\u0010\u008c\u0001\"\u0006\u0008\u008d\u0001\u0010\u008e\u0001R\"\u0010\u008f\u0001\u001a\u0005\u0018\u00010\u0090\u0001X\u0080\u000e\u00a2\u0006\u0012\n\u0000\u001a\u0006\u0008\u0091\u0001\u0010\u0092\u0001\"\u0006\u0008\u0093\u0001\u0010\u0094\u0001R\u001d\u0010\u0095\u0001\u001a\u00020\u0013X\u0080\u000e\u00a2\u0006\u0010\n\u0000\u001a\u0005\u0008\u0096\u0001\u0010\u0015\"\u0005\u0008\u0097\u0001\u0010\u0017R\"\u0010\u0098\u0001\u001a\u0005\u0018\u00010\u0099\u0001X\u0080\u000e\u00a2\u0006\u0012\n\u0000\u001a\u0006\u0008\u009a\u0001\u0010\u009b\u0001\"\u0006\u0008\u009c\u0001\u0010\u009d\u0001\u0082\u0002\u0007\n\u0005\u0008\u009920\u0001\u00a8\u0006\u00b7\u0001"}, d2={"Lokhttp3/OkHttpClient$Builder;", "", "okHttpClient", "Lokhttp3/OkHttpClient;", "(Lokhttp3/OkHttpClient;)V", "()V", "authenticator", "Lokhttp3/Authenticator;", "getAuthenticator$okhttp", "()Lokhttp3/Authenticator;", "setAuthenticator$okhttp", "(Lokhttp3/Authenticator;)V", "cache", "Lokhttp3/Cache;", "getCache$okhttp", "()Lokhttp3/Cache;", "setCache$okhttp", "(Lokhttp3/Cache;)V", "callTimeout", "", "getCallTimeout$okhttp", "()I", "setCallTimeout$okhttp", "(I)V", "certificateChainCleaner", "Lokhttp3/internal/tls/CertificateChainCleaner;", "getCertificateChainCleaner$okhttp", "()Lokhttp3/internal/tls/CertificateChainCleaner;", "setCertificateChainCleaner$okhttp", "(Lokhttp3/internal/tls/CertificateChainCleaner;)V", "certificatePinner", "Lokhttp3/CertificatePinner;", "getCertificatePinner$okhttp", "()Lokhttp3/CertificatePinner;", "setCertificatePinner$okhttp", "(Lokhttp3/CertificatePinner;)V", "connectTimeout", "getConnectTimeout$okhttp", "setConnectTimeout$okhttp", "connectionPool", "Lokhttp3/ConnectionPool;", "getConnectionPool$okhttp", "()Lokhttp3/ConnectionPool;", "setConnectionPool$okhttp", "(Lokhttp3/ConnectionPool;)V", "connectionSpecs", "", "Lokhttp3/ConnectionSpec;", "getConnectionSpecs$okhttp", "()Ljava/util/List;", "setConnectionSpecs$okhttp", "(Ljava/util/List;)V", "cookieJar", "Lokhttp3/CookieJar;", "getCookieJar$okhttp", "()Lokhttp3/CookieJar;", "setCookieJar$okhttp", "(Lokhttp3/CookieJar;)V", "dispatcher", "Lokhttp3/Dispatcher;", "getDispatcher$okhttp", "()Lokhttp3/Dispatcher;", "setDispatcher$okhttp", "(Lokhttp3/Dispatcher;)V", "dns", "Lokhttp3/Dns;", "getDns$okhttp", "()Lokhttp3/Dns;", "setDns$okhttp", "(Lokhttp3/Dns;)V", "eventListenerFactory", "Lokhttp3/EventListener$Factory;", "getEventListenerFactory$okhttp", "()Lokhttp3/EventListener$Factory;", "setEventListenerFactory$okhttp", "(Lokhttp3/EventListener$Factory;)V", "followRedirects", "", "getFollowRedirects$okhttp", "()Z", "setFollowRedirects$okhttp", "(Z)V", "followSslRedirects", "getFollowSslRedirects$okhttp", "setFollowSslRedirects$okhttp", "hostnameVerifier", "Ljavax/net/ssl/HostnameVerifier;", "getHostnameVerifier$okhttp", "()Ljavax/net/ssl/HostnameVerifier;", "setHostnameVerifier$okhttp", "(Ljavax/net/ssl/HostnameVerifier;)V", "interceptors", "", "Lokhttp3/Interceptor;", "getInterceptors$okhttp", "minWebSocketMessageToCompress", "", "getMinWebSocketMessageToCompress$okhttp", "()J", "setMinWebSocketMessageToCompress$okhttp", "(J)V", "networkInterceptors", "getNetworkInterceptors$okhttp", "pingInterval", "getPingInterval$okhttp", "setPingInterval$okhttp", "protocols", "Lokhttp3/Protocol;", "getProtocols$okhttp", "setProtocols$okhttp", "proxy", "Ljava/net/Proxy;", "getProxy$okhttp", "()Ljava/net/Proxy;", "setProxy$okhttp", "(Ljava/net/Proxy;)V", "proxyAuthenticator", "getProxyAuthenticator$okhttp", "setProxyAuthenticator$okhttp", "proxySelector", "Ljava/net/ProxySelector;", "getProxySelector$okhttp", "()Ljava/net/ProxySelector;", "setProxySelector$okhttp", "(Ljava/net/ProxySelector;)V", "readTimeout", "getReadTimeout$okhttp", "setReadTimeout$okhttp", "retryOnConnectionFailure", "getRetryOnConnectionFailure$okhttp", "setRetryOnConnectionFailure$okhttp", "routeDatabase", "Lokhttp3/internal/connection/RouteDatabase;", "getRouteDatabase$okhttp", "()Lokhttp3/internal/connection/RouteDatabase;", "setRouteDatabase$okhttp", "(Lokhttp3/internal/connection/RouteDatabase;)V", "socketFactory", "Ljavax/net/SocketFactory;", "getSocketFactory$okhttp", "()Ljavax/net/SocketFactory;", "setSocketFactory$okhttp", "(Ljavax/net/SocketFactory;)V", "sslSocketFactoryOrNull", "Ljavax/net/ssl/SSLSocketFactory;", "getSslSocketFactoryOrNull$okhttp", "()Ljavax/net/ssl/SSLSocketFactory;", "setSslSocketFactoryOrNull$okhttp", "(Ljavax/net/ssl/SSLSocketFactory;)V", "writeTimeout", "getWriteTimeout$okhttp", "setWriteTimeout$okhttp", "x509TrustManagerOrNull", "Ljavax/net/ssl/X509TrustManager;", "getX509TrustManagerOrNull$okhttp", "()Ljavax/net/ssl/X509TrustManager;", "setX509TrustManagerOrNull$okhttp", "(Ljavax/net/ssl/X509TrustManager;)V", "addInterceptor", "block", "Lkotlin/Function1;", "Lokhttp3/Interceptor$Chain;", "Lkotlin/ParameterName;", "name", "chain", "Lokhttp3/Response;", "-addInterceptor", "interceptor", "addNetworkInterceptor", "-addNetworkInterceptor", "build", "duration", "Ljava/time/Duration;", "timeout", "unit", "Ljava/util/concurrent/TimeUnit;", "eventListener", "Lokhttp3/EventListener;", "followProtocolRedirects", "bytes", "interval", "sslSocketFactory", "trustManager", "okhttp"}) + + @Lkotlin/jvm/internal/SourceDebugExtension;(value={"SMAP\nOkHttpClient.kt\nKotlin\n*S Kotlin\n*F\n+ 1 OkHttpClient.kt\nokhttp3/OkHttpClient$Builder\n+ 2 fake.kt\nkotlin/jvm/internal/FakeKt\n*L\n1#1,1079:1\n1#2:1080\n*E\n"}) // invisible + // access flags 0x609 + public static abstract INNERCLASS okhttp3/EventListener$Factory okhttp3/EventListener Factory + // access flags 0x609 + public static abstract INNERCLASS okhttp3/Interceptor$Chain okhttp3/Interceptor Chain + // access flags 0x19 + public final static INNERCLASS okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient Builder + // access flags 0x19 + public final static INNERCLASS okhttp3/OkHttpClient$Builder$addInterceptor$2 null null + // access flags 0x19 + public final static INNERCLASS okhttp3/OkHttpClient$Builder$addNetworkInterceptor$2 null null + // access flags 0x19 + public final static INNERCLASS okhttp3/OkHttpClient$Companion okhttp3/OkHttpClient Companion + // access flags 0x19 + public final static INNERCLASS okhttp3/internal/platform/Platform$Companion okhttp3/internal/platform/Platform Companion + // access flags 0x19 + public final static INNERCLASS okhttp3/internal/tls/CertificateChainCleaner$Companion okhttp3/internal/tls/CertificateChainCleaner Companion + + // access flags 0x2 + private Lokhttp3/Dispatcher; dispatcher + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Lokhttp3/ConnectionPool; connectionPool + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x12 + // signature Ljava/util/List; + // declaration: interceptors extends java.util.List + private final Ljava/util/List; interceptors + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x12 + // signature Ljava/util/List; + // declaration: networkInterceptors extends java.util.List + private final Ljava/util/List; networkInterceptors + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Lokhttp3/EventListener$Factory; eventListenerFactory + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Z retryOnConnectionFailure + + // access flags 0x2 + private Lokhttp3/Authenticator; authenticator + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Z followRedirects + + // access flags 0x2 + private Z followSslRedirects + + // access flags 0x2 + private Lokhttp3/CookieJar; cookieJar + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Lokhttp3/Cache; cache + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + private Lokhttp3/Dns; dns + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Ljava/net/Proxy; proxy + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + private Ljava/net/ProxySelector; proxySelector + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + private Lokhttp3/Authenticator; proxyAuthenticator + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Ljavax/net/SocketFactory; socketFactory + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Ljavax/net/ssl/SSLSocketFactory; sslSocketFactoryOrNull + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + private Ljavax/net/ssl/X509TrustManager; x509TrustManagerOrNull + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + // signature Ljava/util/List; + // declaration: connectionSpecs extends java.util.List + private Ljava/util/List; connectionSpecs + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + // signature Ljava/util/List<+Lokhttp3/Protocol;>; + // declaration: protocols extends java.util.List + private Ljava/util/List; protocols + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Ljavax/net/ssl/HostnameVerifier; hostnameVerifier + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Lokhttp3/CertificatePinner; certificatePinner + @Lorg/jetbrains/annotations/NotNull;() // invisible + + // access flags 0x2 + private Lokhttp3/internal/tls/CertificateChainCleaner; certificateChainCleaner + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x2 + private I callTimeout + + // access flags 0x2 + private I connectTimeout + + // access flags 0x2 + private I readTimeout + + // access flags 0x2 + private I writeTimeout + + // access flags 0x2 + private I pingInterval + + // access flags 0x2 + private J minWebSocketMessageToCompress + + // access flags 0x2 + private Lokhttp3/internal/connection/RouteDatabase; routeDatabase + @Lorg/jetbrains/annotations/Nullable;() // invisible + + // access flags 0x1 + public ()V + L0 + LINENUMBER 469 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + L1 + LINENUMBER 470 L1 + ALOAD 0 + NEW okhttp3/Dispatcher + DUP + INVOKESPECIAL okhttp3/Dispatcher. ()V + PUTFIELD okhttp3/OkHttpClient$Builder.dispatcher : Lokhttp3/Dispatcher; + L2 + LINENUMBER 471 L2 + ALOAD 0 + NEW okhttp3/ConnectionPool + DUP + INVOKESPECIAL okhttp3/ConnectionPool. ()V + PUTFIELD okhttp3/OkHttpClient$Builder.connectionPool : Lokhttp3/ConnectionPool; + L3 + LINENUMBER 472 L3 + ALOAD 0 + NEW java/util/ArrayList + DUP + INVOKESPECIAL java/util/ArrayList. ()V + CHECKCAST java/util/List + L4 + LINENUMBER 472 L4 + PUTFIELD okhttp3/OkHttpClient$Builder.interceptors : Ljava/util/List; + L5 + LINENUMBER 473 L5 + ALOAD 0 + NEW java/util/ArrayList + DUP + INVOKESPECIAL java/util/ArrayList. ()V + CHECKCAST java/util/List + L6 + LINENUMBER 473 L6 + PUTFIELD okhttp3/OkHttpClient$Builder.networkInterceptors : Ljava/util/List; + L7 + LINENUMBER 474 L7 + ALOAD 0 + GETSTATIC okhttp3/EventListener.NONE : Lokhttp3/EventListener; + INVOKESTATIC okhttp3/internal/Util.asFactory (Lokhttp3/EventListener;)Lokhttp3/EventListener$Factory; + PUTFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + L8 + LINENUMBER 475 L8 + ALOAD 0 + ICONST_1 + PUTFIELD okhttp3/OkHttpClient$Builder.retryOnConnectionFailure : Z + L9 + LINENUMBER 476 L9 + ALOAD 0 + GETSTATIC okhttp3/Authenticator.NONE : Lokhttp3/Authenticator; + PUTFIELD okhttp3/OkHttpClient$Builder.authenticator : Lokhttp3/Authenticator; + L10 + LINENUMBER 477 L10 + ALOAD 0 + ICONST_1 + PUTFIELD okhttp3/OkHttpClient$Builder.followRedirects : Z + L11 + LINENUMBER 478 L11 + ALOAD 0 + ICONST_1 + PUTFIELD okhttp3/OkHttpClient$Builder.followSslRedirects : Z + L12 + LINENUMBER 479 L12 + ALOAD 0 + GETSTATIC okhttp3/CookieJar.NO_COOKIES : Lokhttp3/CookieJar; + PUTFIELD okhttp3/OkHttpClient$Builder.cookieJar : Lokhttp3/CookieJar; + L13 + LINENUMBER 481 L13 + ALOAD 0 + GETSTATIC okhttp3/Dns.SYSTEM : Lokhttp3/Dns; + PUTFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + L14 + LINENUMBER 484 L14 + ALOAD 0 + GETSTATIC okhttp3/Authenticator.NONE : Lokhttp3/Authenticator; + PUTFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + L15 + LINENUMBER 485 L15 + ALOAD 0 + INVOKESTATIC javax/net/SocketFactory.getDefault ()Ljavax/net/SocketFactory; + DUP + LDC "getDefault()" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V + PUTFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + L16 + LINENUMBER 488 L16 + ALOAD 0 + GETSTATIC okhttp3/OkHttpClient.Companion : Lokhttp3/OkHttpClient$Companion; + INVOKEVIRTUAL okhttp3/OkHttpClient$Companion.getDEFAULT_CONNECTION_SPECS$okhttp ()Ljava/util/List; + PUTFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + L17 + LINENUMBER 489 L17 + ALOAD 0 + GETSTATIC okhttp3/OkHttpClient.Companion : Lokhttp3/OkHttpClient$Companion; + INVOKEVIRTUAL okhttp3/OkHttpClient$Companion.getDEFAULT_PROTOCOLS$okhttp ()Ljava/util/List; + PUTFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + L18 + LINENUMBER 490 L18 + ALOAD 0 + GETSTATIC okhttp3/internal/tls/OkHostnameVerifier.INSTANCE : Lokhttp3/internal/tls/OkHostnameVerifier; + CHECKCAST javax/net/ssl/HostnameVerifier + PUTFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + L19 + LINENUMBER 491 L19 + ALOAD 0 + GETSTATIC okhttp3/CertificatePinner.DEFAULT : Lokhttp3/CertificatePinner; + PUTFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + L20 + LINENUMBER 494 L20 + ALOAD 0 + SIPUSH 10000 + PUTFIELD okhttp3/OkHttpClient$Builder.connectTimeout : I + L21 + LINENUMBER 495 L21 + ALOAD 0 + SIPUSH 10000 + PUTFIELD okhttp3/OkHttpClient$Builder.readTimeout : I + L22 + LINENUMBER 496 L22 + ALOAD 0 + SIPUSH 10000 + PUTFIELD okhttp3/OkHttpClient$Builder.writeTimeout : I + L23 + LINENUMBER 498 L23 + ALOAD 0 + LDC 1024 + PUTFIELD okhttp3/OkHttpClient$Builder.minWebSocketMessageToCompress : J + L24 + LINENUMBER 469 L24 + RETURN + L25 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L25 0 + MAXSTACK = 4 + MAXLOCALS = 1 + + // access flags 0x11 + public final getDispatcher$okhttp()Lokhttp3/Dispatcher; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 470 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.dispatcher : Lokhttp3/Dispatcher; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setDispatcher$okhttp(Lokhttp3/Dispatcher;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 470 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.dispatcher : Lokhttp3/Dispatcher; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/Dispatcher; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getConnectionPool$okhttp()Lokhttp3/ConnectionPool; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 471 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.connectionPool : Lokhttp3/ConnectionPool; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setConnectionPool$okhttp(Lokhttp3/ConnectionPool;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 471 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.connectionPool : Lokhttp3/ConnectionPool; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/ConnectionPool; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List getInterceptors$okhttp() + public final getInterceptors$okhttp()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 472 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.interceptors : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List getNetworkInterceptors$okhttp() + public final getNetworkInterceptors$okhttp()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 473 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.networkInterceptors : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final getEventListenerFactory$okhttp()Lokhttp3/EventListener$Factory; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 474 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setEventListenerFactory$okhttp(Lokhttp3/EventListener$Factory;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 474 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/EventListener$Factory; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getRetryOnConnectionFailure$okhttp()Z + L0 + LINENUMBER 475 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.retryOnConnectionFailure : Z + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setRetryOnConnectionFailure$okhttp(Z)V + L0 + LINENUMBER 475 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.retryOnConnectionFailure : Z + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Z L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getAuthenticator$okhttp()Lokhttp3/Authenticator; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 476 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.authenticator : Lokhttp3/Authenticator; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setAuthenticator$okhttp(Lokhttp3/Authenticator;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 476 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.authenticator : Lokhttp3/Authenticator; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/Authenticator; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getFollowRedirects$okhttp()Z + L0 + LINENUMBER 477 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.followRedirects : Z + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setFollowRedirects$okhttp(Z)V + L0 + LINENUMBER 477 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.followRedirects : Z + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Z L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getFollowSslRedirects$okhttp()Z + L0 + LINENUMBER 478 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.followSslRedirects : Z + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setFollowSslRedirects$okhttp(Z)V + L0 + LINENUMBER 478 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.followSslRedirects : Z + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Z L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getCookieJar$okhttp()Lokhttp3/CookieJar; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 479 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.cookieJar : Lokhttp3/CookieJar; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setCookieJar$okhttp(Lokhttp3/CookieJar;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 479 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.cookieJar : Lokhttp3/CookieJar; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/CookieJar; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getCache$okhttp()Lokhttp3/Cache; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 480 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.cache : Lokhttp3/Cache; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setCache$okhttp(Lokhttp3/Cache;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 480 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.cache : Lokhttp3/Cache; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Lokhttp3/Cache; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getDns$okhttp()Lokhttp3/Dns; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 481 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setDns$okhttp(Lokhttp3/Dns;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 481 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/Dns; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getProxy$okhttp()Ljava/net/Proxy; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 482 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.proxy : Ljava/net/Proxy; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setProxy$okhttp(Ljava/net/Proxy;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 482 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxy : Ljava/net/Proxy; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Ljava/net/Proxy; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getProxySelector$okhttp()Ljava/net/ProxySelector; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 483 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.proxySelector : Ljava/net/ProxySelector; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setProxySelector$okhttp(Ljava/net/ProxySelector;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 483 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxySelector : Ljava/net/ProxySelector; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Ljava/net/ProxySelector; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getProxyAuthenticator$okhttp()Lokhttp3/Authenticator; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 484 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setProxyAuthenticator$okhttp(Lokhttp3/Authenticator;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 484 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/Authenticator; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getSocketFactory$okhttp()Ljavax/net/SocketFactory; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 485 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setSocketFactory$okhttp(Ljavax/net/SocketFactory;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 485 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Ljavax/net/SocketFactory; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getSslSocketFactoryOrNull$okhttp()Ljavax/net/ssl/SSLSocketFactory; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 486 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setSslSocketFactoryOrNull$okhttp(Ljavax/net/ssl/SSLSocketFactory;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 486 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Ljavax/net/ssl/SSLSocketFactory; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getX509TrustManagerOrNull$okhttp()Ljavax/net/ssl/X509TrustManager; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 487 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setX509TrustManagerOrNull$okhttp(Ljavax/net/ssl/X509TrustManager;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 487 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Ljavax/net/ssl/X509TrustManager; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List getConnectionSpecs$okhttp() + public final getConnectionSpecs$okhttp()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 488 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + // signature (Ljava/util/List;)V + // declaration: void setConnectionSpecs$okhttp(java.util.List) + public final setConnectionSpecs$okhttp(Ljava/util/List;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 488 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Ljava/util/List; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List getProtocols$okhttp() + public final getProtocols$okhttp()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 489 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + // signature (Ljava/util/List<+Lokhttp3/Protocol;>;)V + // declaration: void setProtocols$okhttp(java.util.List) + public final setProtocols$okhttp(Ljava/util/List;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 489 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Ljava/util/List; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getHostnameVerifier$okhttp()Ljavax/net/ssl/HostnameVerifier; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 490 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setHostnameVerifier$okhttp(Ljavax/net/ssl/HostnameVerifier;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 490 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Ljavax/net/ssl/HostnameVerifier; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getCertificatePinner$okhttp()Lokhttp3/CertificatePinner; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 491 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setCertificatePinner$okhttp(Lokhttp3/CertificatePinner;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 491 L1 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + RETURN + L2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE Lokhttp3/CertificatePinner; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getCertificateChainCleaner$okhttp()Lokhttp3/internal/tls/CertificateChainCleaner; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 492 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.certificateChainCleaner : Lokhttp3/internal/tls/CertificateChainCleaner; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setCertificateChainCleaner$okhttp(Lokhttp3/internal/tls/CertificateChainCleaner;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 492 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.certificateChainCleaner : Lokhttp3/internal/tls/CertificateChainCleaner; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Lokhttp3/internal/tls/CertificateChainCleaner; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getCallTimeout$okhttp()I + L0 + LINENUMBER 493 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.callTimeout : I + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setCallTimeout$okhttp(I)V + L0 + LINENUMBER 493 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.callTimeout : I + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE I L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getConnectTimeout$okhttp()I + L0 + LINENUMBER 494 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.connectTimeout : I + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setConnectTimeout$okhttp(I)V + L0 + LINENUMBER 494 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.connectTimeout : I + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE I L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getReadTimeout$okhttp()I + L0 + LINENUMBER 495 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.readTimeout : I + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setReadTimeout$okhttp(I)V + L0 + LINENUMBER 495 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.readTimeout : I + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE I L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getWriteTimeout$okhttp()I + L0 + LINENUMBER 496 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.writeTimeout : I + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setWriteTimeout$okhttp(I)V + L0 + LINENUMBER 496 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.writeTimeout : I + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE I L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getPingInterval$okhttp()I + L0 + LINENUMBER 497 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.pingInterval : I + IRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setPingInterval$okhttp(I)V + L0 + LINENUMBER 497 L0 + ALOAD 0 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.pingInterval : I + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE I L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x11 + public final getMinWebSocketMessageToCompress$okhttp()J + L0 + LINENUMBER 498 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.minWebSocketMessageToCompress : J + LRETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 2 + MAXLOCALS = 1 + + // access flags 0x11 + public final setMinWebSocketMessageToCompress$okhttp(J)V + L0 + LINENUMBER 498 L0 + ALOAD 0 + LLOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.minWebSocketMessageToCompress : J + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE J L0 L1 1 + MAXSTACK = 3 + MAXLOCALS = 3 + + // access flags 0x11 + public final getRouteDatabase$okhttp()Lokhttp3/internal/connection/RouteDatabase; + @Lorg/jetbrains/annotations/Nullable;() // invisible + L0 + LINENUMBER 499 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final setRouteDatabase$okhttp(Lokhttp3/internal/connection/RouteDatabase;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 499 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + RETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + LOCALVARIABLE Lokhttp3/internal/connection/RouteDatabase; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public (Lokhttp3/OkHttpClient;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "okHttpClient" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 501 L1 + ALOAD 0 + INVOKESPECIAL okhttp3/OkHttpClient$Builder. ()V + L2 + LINENUMBER 502 L2 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.dispatcher ()Lokhttp3/Dispatcher; + PUTFIELD okhttp3/OkHttpClient$Builder.dispatcher : Lokhttp3/Dispatcher; + L3 + LINENUMBER 503 L3 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.connectionPool ()Lokhttp3/ConnectionPool; + PUTFIELD okhttp3/OkHttpClient$Builder.connectionPool : Lokhttp3/ConnectionPool; + L4 + LINENUMBER 504 L4 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.interceptors : Ljava/util/List; + CHECKCAST java/util/Collection + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.interceptors ()Ljava/util/List; + CHECKCAST java/lang/Iterable + INVOKESTATIC kotlin/collections/CollectionsKt.addAll (Ljava/util/Collection;Ljava/lang/Iterable;)Z + POP + L5 + LINENUMBER 505 L5 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.networkInterceptors : Ljava/util/List; + CHECKCAST java/util/Collection + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.networkInterceptors ()Ljava/util/List; + CHECKCAST java/lang/Iterable + INVOKESTATIC kotlin/collections/CollectionsKt.addAll (Ljava/util/Collection;Ljava/lang/Iterable;)Z + POP + L6 + LINENUMBER 506 L6 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.eventListenerFactory ()Lokhttp3/EventListener$Factory; + PUTFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + L7 + LINENUMBER 507 L7 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.retryOnConnectionFailure ()Z + PUTFIELD okhttp3/OkHttpClient$Builder.retryOnConnectionFailure : Z + L8 + LINENUMBER 508 L8 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.authenticator ()Lokhttp3/Authenticator; + PUTFIELD okhttp3/OkHttpClient$Builder.authenticator : Lokhttp3/Authenticator; + L9 + LINENUMBER 509 L9 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.followRedirects ()Z + PUTFIELD okhttp3/OkHttpClient$Builder.followRedirects : Z + L10 + LINENUMBER 510 L10 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.followSslRedirects ()Z + PUTFIELD okhttp3/OkHttpClient$Builder.followSslRedirects : Z + L11 + LINENUMBER 511 L11 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.cookieJar ()Lokhttp3/CookieJar; + PUTFIELD okhttp3/OkHttpClient$Builder.cookieJar : Lokhttp3/CookieJar; + L12 + LINENUMBER 512 L12 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.cache ()Lokhttp3/Cache; + PUTFIELD okhttp3/OkHttpClient$Builder.cache : Lokhttp3/Cache; + L13 + LINENUMBER 513 L13 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.dns ()Lokhttp3/Dns; + PUTFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + L14 + LINENUMBER 514 L14 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.proxy ()Ljava/net/Proxy; + PUTFIELD okhttp3/OkHttpClient$Builder.proxy : Ljava/net/Proxy; + L15 + LINENUMBER 515 L15 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.proxySelector ()Ljava/net/ProxySelector; + PUTFIELD okhttp3/OkHttpClient$Builder.proxySelector : Ljava/net/ProxySelector; + L16 + LINENUMBER 516 L16 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.proxyAuthenticator ()Lokhttp3/Authenticator; + PUTFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + L17 + LINENUMBER 517 L17 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.socketFactory ()Ljavax/net/SocketFactory; + PUTFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + L18 + LINENUMBER 518 L18 + ALOAD 0 + ALOAD 1 + INVOKESTATIC okhttp3/OkHttpClient.access$getSslSocketFactoryOrNull$p (Lokhttp3/OkHttpClient;)Ljavax/net/ssl/SSLSocketFactory; + PUTFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + L19 + LINENUMBER 519 L19 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.x509TrustManager ()Ljavax/net/ssl/X509TrustManager; + PUTFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + L20 + LINENUMBER 520 L20 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.connectionSpecs ()Ljava/util/List; + PUTFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + L21 + LINENUMBER 521 L21 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.protocols ()Ljava/util/List; + PUTFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + L22 + LINENUMBER 522 L22 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; + PUTFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + L23 + LINENUMBER 523 L23 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.certificatePinner ()Lokhttp3/CertificatePinner; + PUTFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + L24 + LINENUMBER 524 L24 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.certificateChainCleaner ()Lokhttp3/internal/tls/CertificateChainCleaner; + PUTFIELD okhttp3/OkHttpClient$Builder.certificateChainCleaner : Lokhttp3/internal/tls/CertificateChainCleaner; + L25 + LINENUMBER 525 L25 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.callTimeoutMillis ()I + PUTFIELD okhttp3/OkHttpClient$Builder.callTimeout : I + L26 + LINENUMBER 526 L26 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.connectTimeoutMillis ()I + PUTFIELD okhttp3/OkHttpClient$Builder.connectTimeout : I + L27 + LINENUMBER 527 L27 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.readTimeoutMillis ()I + PUTFIELD okhttp3/OkHttpClient$Builder.readTimeout : I + L28 + LINENUMBER 528 L28 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.writeTimeoutMillis ()I + PUTFIELD okhttp3/OkHttpClient$Builder.writeTimeout : I + L29 + LINENUMBER 529 L29 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.pingIntervalMillis ()I + PUTFIELD okhttp3/OkHttpClient$Builder.pingInterval : I + L30 + LINENUMBER 530 L30 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.minWebSocketMessageToCompress ()J + PUTFIELD okhttp3/OkHttpClient$Builder.minWebSocketMessageToCompress : J + L31 + LINENUMBER 531 L31 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL okhttp3/OkHttpClient.getRouteDatabase ()Lokhttp3/internal/connection/RouteDatabase; + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L32 + LINENUMBER 532 L32 + RETURN + L33 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L33 0 + LOCALVARIABLE okHttpClient Lokhttp3/OkHttpClient; L0 L33 1 + MAXSTACK = 3 + MAXLOCALS = 2 + + // access flags 0x11 + public final dispatcher(Lokhttp3/Dispatcher;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "dispatcher" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 537 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 538 L3 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.dispatcher : Lokhttp3/Dispatcher; + L4 + LINENUMBER 539 L4 + NOP + L5 + LINENUMBER 537 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 539 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$dispatcher$1 I L3 L5 4 + LOCALVARIABLE $this$dispatcher_u24lambda_u240 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE dispatcher Lokhttp3/Dispatcher; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final connectionPool(Lokhttp3/ConnectionPool;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "connectionPool" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 546 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 547 L3 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.connectionPool : Lokhttp3/ConnectionPool; + L4 + LINENUMBER 548 L4 + NOP + L5 + LINENUMBER 546 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 548 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$connectionPool$1 I L3 L5 4 + LOCALVARIABLE $this$connectionPool_u24lambda_u241 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE connectionPool Lokhttp3/ConnectionPool; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List interceptors() + public final interceptors()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 555 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.interceptors : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final addInterceptor(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "interceptor" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 557 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 558 L3 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.interceptors : Ljava/util/List; + CHECKCAST java/util/Collection + ALOAD 1 + INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z (itf) + POP + L4 + LINENUMBER 559 L4 + NOP + L5 + LINENUMBER 557 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 559 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$addInterceptor$1 I L3 L5 4 + LOCALVARIABLE $this$addInterceptor_u24lambda_u242 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE interceptor Lokhttp3/Interceptor; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + // signature (Lkotlin/jvm/functions/Function1<-Lokhttp3/Interceptor$Chain;Lokhttp3/Response;>;)Lokhttp3/OkHttpClient$Builder; + // declaration: okhttp3.OkHttpClient$Builder -addInterceptor(kotlin.jvm.functions.Function1) + public final -addInterceptor(Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; + @Lkotlin/jvm/JvmName;(name="-addInterceptor") // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "block" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + ICONST_0 + ISTORE 2 + L1 + LINENUMBER 563 L1 + ALOAD 0 + NEW okhttp3/OkHttpClient$Builder$addInterceptor$2 + DUP + ALOAD 1 + INVOKESPECIAL okhttp3/OkHttpClient$Builder$addInterceptor$2. (Lkotlin/jvm/functions/Function1;)V + CHECKCAST okhttp3/Interceptor + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; + ARETURN + L2 + LOCALVARIABLE $i$f$-addInterceptor I L1 L2 2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE block Lkotlin/jvm/functions/Function1; L0 L2 1 + MAXSTACK = 4 + MAXLOCALS = 3 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List networkInterceptors() + public final networkInterceptors()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 570 L0 + ALOAD 0 + GETFIELD okhttp3/OkHttpClient$Builder.networkInterceptors : Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final addNetworkInterceptor(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "interceptor" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 572 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 573 L3 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.networkInterceptors : Ljava/util/List; + CHECKCAST java/util/Collection + ALOAD 1 + INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z (itf) + POP + L4 + LINENUMBER 574 L4 + NOP + L5 + LINENUMBER 572 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 574 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$addNetworkInterceptor$1 I L3 L5 4 + LOCALVARIABLE $this$addNetworkInterceptor_u24lambda_u243 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE interceptor Lokhttp3/Interceptor; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + // signature (Lkotlin/jvm/functions/Function1<-Lokhttp3/Interceptor$Chain;Lokhttp3/Response;>;)Lokhttp3/OkHttpClient$Builder; + // declaration: okhttp3.OkHttpClient$Builder -addNetworkInterceptor(kotlin.jvm.functions.Function1) + public final -addNetworkInterceptor(Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; + @Lkotlin/jvm/JvmName;(name="-addNetworkInterceptor") // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "block" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + ICONST_0 + ISTORE 2 + L1 + LINENUMBER 578 L1 + ALOAD 0 + NEW okhttp3/OkHttpClient$Builder$addNetworkInterceptor$2 + DUP + ALOAD 1 + INVOKESPECIAL okhttp3/OkHttpClient$Builder$addNetworkInterceptor$2. (Lkotlin/jvm/functions/Function1;)V + CHECKCAST okhttp3/Interceptor + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; + ARETURN + L2 + LOCALVARIABLE $i$f$-addNetworkInterceptor I L1 L2 2 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L2 0 + LOCALVARIABLE block Lkotlin/jvm/functions/Function1; L0 L2 1 + MAXSTACK = 4 + MAXLOCALS = 3 + + // access flags 0x11 + public final eventListener(Lokhttp3/EventListener;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "eventListener" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 586 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 587 L3 + ALOAD 3 + ALOAD 1 + INVOKESTATIC okhttp3/internal/Util.asFactory (Lokhttp3/EventListener;)Lokhttp3/EventListener$Factory; + PUTFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + L4 + LINENUMBER 588 L4 + NOP + L5 + LINENUMBER 586 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 588 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$eventListener$1 I L3 L5 4 + LOCALVARIABLE $this$eventListener_u24lambda_u244 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE eventListener Lokhttp3/EventListener; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final eventListenerFactory(Lokhttp3/EventListener$Factory;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "eventListenerFactory" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 596 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 597 L3 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.eventListenerFactory : Lokhttp3/EventListener$Factory; + L4 + LINENUMBER 598 L4 + NOP + L5 + LINENUMBER 596 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 598 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$eventListenerFactory$1 I L3 L5 4 + LOCALVARIABLE $this$eventListenerFactory_u24lambda_u245 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE eventListenerFactory Lokhttp3/EventListener$Factory; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final retryOnConnectionFailure(Z)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 618 L0 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L1 + ICONST_0 + ISTORE 4 + L2 + LINENUMBER 619 L2 + ALOAD 3 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.retryOnConnectionFailure : Z + L3 + LINENUMBER 620 L3 + NOP + L4 + LINENUMBER 618 L4 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L5 + LINENUMBER 620 L5 + ARETURN + L6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$retryOnConnectionFailure$1 I L2 L4 4 + LOCALVARIABLE $this$retryOnConnectionFailure_u24lambda_u246 Lokhttp3/OkHttpClient$Builder; L1 L4 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L6 0 + LOCALVARIABLE retryOnConnectionFailure Z L0 L6 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final authenticator(Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "authenticator" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 628 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 629 L3 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.authenticator : Lokhttp3/Authenticator; + L4 + LINENUMBER 630 L4 + NOP + L5 + LINENUMBER 628 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 630 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$authenticator$1 I L3 L5 4 + LOCALVARIABLE $this$authenticator_u24lambda_u247 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE authenticator Lokhttp3/Authenticator; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final followRedirects(Z)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 633 L0 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L1 + ICONST_0 + ISTORE 4 + L2 + LINENUMBER 634 L2 + ALOAD 3 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.followRedirects : Z + L3 + LINENUMBER 635 L3 + NOP + L4 + LINENUMBER 633 L4 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L5 + LINENUMBER 635 L5 + ARETURN + L6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$followRedirects$1 I L2 L4 4 + LOCALVARIABLE $this$followRedirects_u24lambda_u248 Lokhttp3/OkHttpClient$Builder; L1 L4 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L6 0 + LOCALVARIABLE followRedirects Z L0 L6 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final followSslRedirects(Z)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 643 L0 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L1 + ICONST_0 + ISTORE 4 + L2 + LINENUMBER 644 L2 + ALOAD 3 + ILOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.followSslRedirects : Z + L3 + LINENUMBER 645 L3 + NOP + L4 + LINENUMBER 643 L4 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L5 + LINENUMBER 645 L5 + ARETURN + L6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$followSslRedirects$1 I L2 L4 4 + LOCALVARIABLE $this$followSslRedirects_u24lambda_u249 Lokhttp3/OkHttpClient$Builder; L1 L4 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L6 0 + LOCALVARIABLE followProtocolRedirects Z L0 L6 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final cookieJar(Lokhttp3/CookieJar;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "cookieJar" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 653 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 654 L3 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.cookieJar : Lokhttp3/CookieJar; + L4 + LINENUMBER 655 L4 + NOP + L5 + LINENUMBER 653 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 655 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$cookieJar$1 I L3 L5 4 + LOCALVARIABLE $this$cookieJar_u24lambda_u2410 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE cookieJar Lokhttp3/CookieJar; L0 L7 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final cache(Lokhttp3/Cache;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 658 L0 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L1 + ICONST_0 + ISTORE 4 + L2 + LINENUMBER 659 L2 + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.cache : Lokhttp3/Cache; + L3 + LINENUMBER 660 L3 + NOP + L4 + LINENUMBER 658 L4 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L5 + LINENUMBER 660 L5 + ARETURN + L6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$cache$1 I L2 L4 4 + LOCALVARIABLE $this$cache_u24lambda_u2411 Lokhttp3/OkHttpClient$Builder; L1 L4 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L6 0 + LOCALVARIABLE cache Lokhttp3/Cache; L0 L6 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final dns(Lokhttp3/Dns;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "dns" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 667 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 668 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 669 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 671 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder okhttp3/Dns okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.dns : Lokhttp3/Dns; + L6 + LINENUMBER 672 L6 + NOP + L7 + LINENUMBER 667 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 672 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$dns$1 I L3 L7 4 + LOCALVARIABLE $this$dns_u24lambda_u2412 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE dns Lokhttp3/Dns; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final proxy(Ljava/net/Proxy;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + L0 + LINENUMBER 679 L0 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L1 + ICONST_0 + ISTORE 4 + L2 + LINENUMBER 680 L2 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.proxy : Ljava/net/Proxy; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L3 + L4 + LINENUMBER 681 L4 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L3 + LINENUMBER 683 L3 + FRAME FULL [okhttp3/OkHttpClient$Builder java/net/Proxy okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxy : Ljava/net/Proxy; + L5 + LINENUMBER 684 L5 + NOP + L6 + LINENUMBER 679 L6 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L7 + LINENUMBER 684 L7 + ARETURN + L8 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$proxy$1 I L2 L6 4 + LOCALVARIABLE $this$proxy_u24lambda_u2413 Lokhttp3/OkHttpClient$Builder; L1 L6 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L8 0 + LOCALVARIABLE proxy Ljava/net/Proxy; L0 L8 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final proxySelector(Ljava/net/ProxySelector;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "proxySelector" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 693 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 694 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.proxySelector : Ljava/net/ProxySelector; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 695 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 698 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder java/net/ProxySelector okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxySelector : Ljava/net/ProxySelector; + L6 + LINENUMBER 699 L6 + NOP + L7 + LINENUMBER 693 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 699 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$proxySelector$1 I L3 L7 4 + LOCALVARIABLE $this$proxySelector_u24lambda_u2414 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE proxySelector Ljava/net/ProxySelector; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final proxyAuthenticator(Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "proxyAuthenticator" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 707 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 708 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 709 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 712 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder okhttp3/Authenticator okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.proxyAuthenticator : Lokhttp3/Authenticator; + L6 + LINENUMBER 713 L6 + NOP + L7 + LINENUMBER 707 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 713 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$proxyAuthenticator$1 I L3 L7 4 + LOCALVARIABLE $this$proxyAuthenticator_u24lambda_u2415 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE proxyAuthenticator Lokhttp3/Authenticator; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final socketFactory(Ljavax/net/SocketFactory;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "socketFactory" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 722 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 723 L3 + ALOAD 1 + INSTANCEOF javax/net/ssl/SSLSocketFactory + IFNE L4 + ICONST_1 + GOTO L5 + L4 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/SocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ICONST_0 + L5 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/SocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [I] + IFNE L6 + L7 + LINENUMBER 1080 L7 + ICONST_0 + ISTORE 5 + L8 + LINENUMBER 723 L8 + LDC "socketFactory instanceof SSLSocketFactory" + L9 + LINENUMBER 723 L9 + ASTORE 5 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 5 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L6 + LINENUMBER 725 L6 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/SocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L10 + L11 + LINENUMBER 726 L11 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L10 + LINENUMBER 729 L10 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/SocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.socketFactory : Ljavax/net/SocketFactory; + L12 + LINENUMBER 730 L12 + NOP + L13 + LINENUMBER 722 L13 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L14 + LINENUMBER 730 L14 + ARETURN + L15 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$socketFactory$1$1 I L8 L9 5 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$socketFactory$1 I L3 L13 4 + LOCALVARIABLE $this$socketFactory_u24lambda_u2417 Lokhttp3/OkHttpClient$Builder; L2 L13 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L15 0 + LOCALVARIABLE socketFactory Ljavax/net/SocketFactory; L0 L15 1 + MAXSTACK = 3 + MAXLOCALS = 6 + + // DEPRECATED + // access flags 0x20011 + public final sslSocketFactory(Ljavax/net/ssl/SSLSocketFactory;)Lokhttp3/OkHttpClient$Builder; + @Lkotlin/Deprecated;(message="Use the sslSocketFactory overload that accepts a X509TrustManager.", level=Lkotlin/DeprecationLevel;.ERROR) + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "sslSocketFactory" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 745 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 746 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 747 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 750 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/ssl/SSLSocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + L6 + LINENUMBER 751 L6 + ALOAD 3 + GETSTATIC okhttp3/internal/platform/Platform.Companion : Lokhttp3/internal/platform/Platform$Companion; + INVOKEVIRTUAL okhttp3/internal/platform/Platform$Companion.get ()Lokhttp3/internal/platform/Platform; + ALOAD 1 + INVOKEVIRTUAL okhttp3/internal/platform/Platform.trustManager (Ljavax/net/ssl/SSLSocketFactory;)Ljavax/net/ssl/X509TrustManager; + DUP + IFNONNULL L7 + POP + NEW java/lang/IllegalStateException + DUP + L8 + LINENUMBER 752 L8 + NEW java/lang/StringBuilder + DUP + INVOKESPECIAL java/lang/StringBuilder. ()V + LDC "Unable to extract the trust manager on " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + GETSTATIC okhttp3/internal/platform/Platform.Companion : Lokhttp3/internal/platform/Platform$Companion; + INVOKEVIRTUAL okhttp3/internal/platform/Platform$Companion.get ()Lokhttp3/internal/platform/Platform; + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; + LDC ", sslSocketFactory is " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + L9 + LINENUMBER 753 L9 + ALOAD 1 + INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; + L10 + LINENUMBER 752 L10 + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; + INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; + L11 + LINENUMBER 751 L11 + INVOKESPECIAL java/lang/IllegalStateException. (Ljava/lang/String;)V + ATHROW + L7 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/ssl/SSLSocketFactory okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [okhttp3/OkHttpClient$Builder javax/net/ssl/X509TrustManager] + PUTFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + L12 + LINENUMBER 754 L12 + ALOAD 3 + GETSTATIC okhttp3/internal/platform/Platform.Companion : Lokhttp3/internal/platform/Platform$Companion; + INVOKEVIRTUAL okhttp3/internal/platform/Platform$Companion.get ()Lokhttp3/internal/platform/Platform; + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + DUP + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNull (Ljava/lang/Object;)V + INVOKEVIRTUAL okhttp3/internal/platform/Platform.buildCertificateChainCleaner (Ljavax/net/ssl/X509TrustManager;)Lokhttp3/internal/tls/CertificateChainCleaner; + PUTFIELD okhttp3/OkHttpClient$Builder.certificateChainCleaner : Lokhttp3/internal/tls/CertificateChainCleaner; + L13 + LINENUMBER 755 L13 + NOP + L14 + LINENUMBER 745 L14 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L15 + LINENUMBER 755 L15 + ARETURN + L16 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$sslSocketFactory$1 I L3 L14 4 + LOCALVARIABLE $this$sslSocketFactory_u24lambda_u2418 Lokhttp3/OkHttpClient$Builder; L2 L14 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L16 0 + LOCALVARIABLE sslSocketFactory Ljavax/net/ssl/SSLSocketFactory; L0 L16 1 + MAXSTACK = 5 + MAXLOCALS = 5 + + // access flags 0x11 + public final sslSocketFactory(Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 1 + LDC "sslSocketFactory" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + ALOAD 2 + LDC "trustManager" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 805 L1 + ALOAD 0 + ASTORE 3 + ALOAD 3 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 4 + L2 + ICONST_0 + ISTORE 5 + L3 + LINENUMBER 806 L3 + ALOAD 1 + ALOAD 4 + GETFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFEQ L4 + ALOAD 2 + ALOAD 4 + GETFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L5 + L4 + LINENUMBER 807 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/ssl/SSLSocketFactory javax/net/ssl/X509TrustManager okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 4 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L5 + LINENUMBER 810 L5 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/ssl/SSLSocketFactory javax/net/ssl/X509TrustManager okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 4 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.sslSocketFactoryOrNull : Ljavax/net/ssl/SSLSocketFactory; + L6 + LINENUMBER 811 L6 + ALOAD 4 + GETSTATIC okhttp3/internal/tls/CertificateChainCleaner.Companion : Lokhttp3/internal/tls/CertificateChainCleaner$Companion; + ALOAD 2 + INVOKEVIRTUAL okhttp3/internal/tls/CertificateChainCleaner$Companion.get (Ljavax/net/ssl/X509TrustManager;)Lokhttp3/internal/tls/CertificateChainCleaner; + PUTFIELD okhttp3/OkHttpClient$Builder.certificateChainCleaner : Lokhttp3/internal/tls/CertificateChainCleaner; + L7 + LINENUMBER 812 L7 + ALOAD 4 + ALOAD 2 + PUTFIELD okhttp3/OkHttpClient$Builder.x509TrustManagerOrNull : Ljavax/net/ssl/X509TrustManager; + L8 + LINENUMBER 813 L8 + NOP + L9 + LINENUMBER 805 L9 + ALOAD 3 + CHECKCAST okhttp3/OkHttpClient$Builder + L10 + LINENUMBER 813 L10 + ARETURN + L11 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$sslSocketFactory$2 I L3 L9 5 + LOCALVARIABLE $this$sslSocketFactory_u24lambda_u2419 Lokhttp3/OkHttpClient$Builder; L2 L9 4 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L11 0 + LOCALVARIABLE sslSocketFactory Ljavax/net/ssl/SSLSocketFactory; L0 L11 1 + LOCALVARIABLE trustManager Ljavax/net/ssl/X509TrustManager; L0 L11 2 + MAXSTACK = 3 + MAXLOCALS = 6 + + // access flags 0x11 + // signature (Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; + // declaration: okhttp3.OkHttpClient$Builder connectionSpecs(java.util.List) + public final connectionSpecs(Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "connectionSpecs" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 815 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 816 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 817 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 820 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + INVOKESTATIC okhttp3/internal/Util.toImmutableList (Ljava/util/List;)Ljava/util/List; + PUTFIELD okhttp3/OkHttpClient$Builder.connectionSpecs : Ljava/util/List; + L6 + LINENUMBER 821 L6 + NOP + L7 + LINENUMBER 815 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 821 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$connectionSpecs$1 I L3 L7 4 + LOCALVARIABLE $this$connectionSpecs_u24lambda_u2420 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE connectionSpecs Ljava/util/List; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + // signature (Ljava/util/List<+Lokhttp3/Protocol;>;)Lokhttp3/OkHttpClient$Builder; + // declaration: okhttp3.OkHttpClient$Builder protocols(java.util.List) + public final protocols(Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "protocols" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 854 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 856 L3 + ALOAD 1 + CHECKCAST java/util/Collection + INVOKESTATIC kotlin/collections/CollectionsKt.toMutableList (Ljava/util/Collection;)Ljava/util/List; + ASTORE 5 + L4 + LINENUMBER 859 L4 + ALOAD 5 + GETSTATIC okhttp3/Protocol.H2_PRIOR_KNOWLEDGE : Lokhttp3/Protocol; + INVOKEINTERFACE java/util/List.contains (Ljava/lang/Object;)Z (itf) + IFNE L5 + ALOAD 5 + GETSTATIC okhttp3/Protocol.HTTP_1_1 : Lokhttp3/Protocol; + INVOKEINTERFACE java/util/List.contains (Ljava/lang/Object;)Z (itf) + IFEQ L6 + L5 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_1 + GOTO L7 + L6 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_0 + L7 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [I] + IFNE L8 + ICONST_0 + ISTORE 6 + L9 + LINENUMBER 860 L9 + NEW java/lang/StringBuilder + DUP + INVOKESPECIAL java/lang/StringBuilder. ()V + LDC "protocols must contain h2_prior_knowledge or http/1.1: " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + ALOAD 5 + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; + INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; + L10 + LINENUMBER 859 L10 + ASTORE 6 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 6 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L8 + LINENUMBER 862 L8 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ALOAD 5 + GETSTATIC okhttp3/Protocol.H2_PRIOR_KNOWLEDGE : Lokhttp3/Protocol; + INVOKEINTERFACE java/util/List.contains (Ljava/lang/Object;)Z (itf) + IFEQ L11 + ALOAD 5 + INVOKEINTERFACE java/util/List.size ()I (itf) + ICONST_1 + IF_ICMPGT L12 + L11 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_1 + GOTO L13 + L12 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_0 + L13 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [I] + IFNE L14 + ICONST_0 + ISTORE 6 + L15 + LINENUMBER 863 L15 + NEW java/lang/StringBuilder + DUP + INVOKESPECIAL java/lang/StringBuilder. ()V + LDC "protocols containing h2_prior_knowledge cannot use other protocols: " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + ALOAD 5 + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; + INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; + L16 + LINENUMBER 862 L16 + ASTORE 6 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 6 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L14 + LINENUMBER 865 L14 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ALOAD 5 + GETSTATIC okhttp3/Protocol.HTTP_1_0 : Lokhttp3/Protocol; + INVOKEINTERFACE java/util/List.contains (Ljava/lang/Object;)Z (itf) + IFNE L17 + ICONST_1 + GOTO L18 + L17 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_0 + L18 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [I] + IFNE L19 + ICONST_0 + ISTORE 6 + L20 + LINENUMBER 866 L20 + NEW java/lang/StringBuilder + DUP + INVOKESPECIAL java/lang/StringBuilder. ()V + LDC "protocols must not contain http/1.0: " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + ALOAD 5 + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder; + INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; + L21 + LINENUMBER 865 L21 + ASTORE 6 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 6 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L19 + LINENUMBER 868 L19 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ALOAD 5 + LDC "null cannot be cast to non-null type kotlin.collections.List" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNull (Ljava/lang/Object;Ljava/lang/String;)V + ALOAD 5 + ACONST_NULL + INVOKEINTERFACE java/util/List.contains (Ljava/lang/Object;)Z (itf) + IFNE L22 + ICONST_1 + GOTO L23 + L22 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ICONST_0 + L23 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [I] + IFNE L24 + ICONST_0 + ISTORE 6 + L25 + LINENUMBER 869 L25 + LDC "protocols must not contain null" + L26 + LINENUMBER 868 L26 + ASTORE 6 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 6 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L24 + LINENUMBER 874 L24 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ALOAD 5 + GETSTATIC okhttp3/Protocol.SPDY_3 : Lokhttp3/Protocol; + INVOKEINTERFACE java/util/List.remove (Ljava/lang/Object;)Z (itf) + POP + L27 + LINENUMBER 876 L27 + ALOAD 5 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L28 + L29 + LINENUMBER 877 L29 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L28 + LINENUMBER 881 L28 + FRAME FULL [okhttp3/OkHttpClient$Builder java/util/List okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I java/util/List] [] + ALOAD 3 + ALOAD 5 + INVOKESTATIC java/util/Collections.unmodifiableList (Ljava/util/List;)Ljava/util/List; + DUP + LDC "unmodifiableList(protocolsCopy)" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V + PUTFIELD okhttp3/OkHttpClient$Builder.protocols : Ljava/util/List; + L30 + LINENUMBER 882 L30 + NOP + L31 + LINENUMBER 854 L31 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L32 + LINENUMBER 882 L32 + ARETURN + L33 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$protocols$1$1 I L9 L10 6 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$protocols$1$2 I L15 L16 6 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$protocols$1$3 I L20 L21 6 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$protocols$1$4 I L25 L26 6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$protocols$1 I L3 L31 4 + LOCALVARIABLE protocolsCopy Ljava/util/List; L4 L31 5 + LOCALVARIABLE $this$protocols_u24lambda_u2425 Lokhttp3/OkHttpClient$Builder; L2 L31 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L33 0 + LOCALVARIABLE protocols Ljava/util/List; L0 L33 1 + MAXSTACK = 4 + MAXLOCALS = 7 + + // access flags 0x11 + public final hostnameVerifier(Ljavax/net/ssl/HostnameVerifier;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "hostnameVerifier" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 890 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 891 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 892 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 895 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder javax/net/ssl/HostnameVerifier okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.hostnameVerifier : Ljavax/net/ssl/HostnameVerifier; + L6 + LINENUMBER 896 L6 + NOP + L7 + LINENUMBER 890 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 896 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$hostnameVerifier$1 I L3 L7 4 + LOCALVARIABLE $this$hostnameVerifier_u24lambda_u2426 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE hostnameVerifier Ljavax/net/ssl/HostnameVerifier; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final certificatePinner(Lokhttp3/CertificatePinner;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "certificatePinner" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 903 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 904 L3 + ALOAD 1 + ALOAD 3 + GETFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + INVOKESTATIC kotlin/jvm/internal/Intrinsics.areEqual (Ljava/lang/Object;Ljava/lang/Object;)Z + IFNE L4 + L5 + LINENUMBER 905 L5 + ALOAD 3 + ACONST_NULL + PUTFIELD okhttp3/OkHttpClient$Builder.routeDatabase : Lokhttp3/internal/connection/RouteDatabase; + L4 + LINENUMBER 908 L4 + FRAME FULL [okhttp3/OkHttpClient$Builder okhttp3/CertificatePinner okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 3 + ALOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.certificatePinner : Lokhttp3/CertificatePinner; + L6 + LINENUMBER 909 L6 + NOP + L7 + LINENUMBER 903 L7 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L8 + LINENUMBER 909 L8 + ARETURN + L9 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$certificatePinner$1 I L3 L7 4 + LOCALVARIABLE $this$certificatePinner_u24lambda_u2427 Lokhttp3/OkHttpClient$Builder; L2 L7 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L9 0 + LOCALVARIABLE certificatePinner Lokhttp3/CertificatePinner; L0 L9 1 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x11 + public final callTimeout(JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 3 + LDC "unit" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 921 L1 + ALOAD 0 + ASTORE 4 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 5 + L2 + ICONST_0 + ISTORE 6 + L3 + LINENUMBER 922 L3 + ALOAD 5 + LDC "timeout" + LLOAD 1 + ALOAD 3 + INVOKESTATIC okhttp3/internal/Util.checkDuration (Ljava/lang/String;JLjava/util/concurrent/TimeUnit;)I + PUTFIELD okhttp3/OkHttpClient$Builder.callTimeout : I + L4 + LINENUMBER 923 L4 + NOP + L5 + LINENUMBER 921 L5 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 923 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$callTimeout$1 I L3 L5 6 + LOCALVARIABLE $this$callTimeout_u24lambda_u2428 Lokhttp3/OkHttpClient$Builder; L2 L5 5 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE timeout J L0 L7 1 + LOCALVARIABLE unit Ljava/util/concurrent/TimeUnit; L0 L7 3 + MAXSTACK = 5 + MAXLOCALS = 7 + + // access flags 0x11 + public final callTimeout(Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; + @Lorg/codehaus/mojo/animal_sniffer/IgnoreJRERequirement;() // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "duration" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 936 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 937 L3 + ALOAD 3 + ALOAD 1 + INVOKEVIRTUAL java/time/Duration.toMillis ()J + GETSTATIC java/util/concurrent/TimeUnit.MILLISECONDS : Ljava/util/concurrent/TimeUnit; + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.callTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + POP + L4 + LINENUMBER 938 L4 + NOP + L5 + LINENUMBER 936 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 938 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$callTimeout$2 I L3 L5 4 + LOCALVARIABLE $this$callTimeout_u24lambda_u2429 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE duration Ljava/time/Duration; L0 L7 1 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x11 + public final connectTimeout(JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 3 + LDC "unit" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 947 L1 + ALOAD 0 + ASTORE 4 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 5 + L2 + ICONST_0 + ISTORE 6 + L3 + LINENUMBER 948 L3 + ALOAD 5 + LDC "timeout" + LLOAD 1 + ALOAD 3 + INVOKESTATIC okhttp3/internal/Util.checkDuration (Ljava/lang/String;JLjava/util/concurrent/TimeUnit;)I + PUTFIELD okhttp3/OkHttpClient$Builder.connectTimeout : I + L4 + LINENUMBER 949 L4 + NOP + L5 + LINENUMBER 947 L5 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 949 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$connectTimeout$1 I L3 L5 6 + LOCALVARIABLE $this$connectTimeout_u24lambda_u2430 Lokhttp3/OkHttpClient$Builder; L2 L5 5 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE timeout J L0 L7 1 + LOCALVARIABLE unit Ljava/util/concurrent/TimeUnit; L0 L7 3 + MAXSTACK = 5 + MAXLOCALS = 7 + + // access flags 0x11 + public final connectTimeout(Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; + @Lorg/codehaus/mojo/animal_sniffer/IgnoreJRERequirement;() // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "duration" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 959 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 960 L3 + ALOAD 3 + ALOAD 1 + INVOKEVIRTUAL java/time/Duration.toMillis ()J + GETSTATIC java/util/concurrent/TimeUnit.MILLISECONDS : Ljava/util/concurrent/TimeUnit; + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.connectTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + POP + L4 + LINENUMBER 961 L4 + NOP + L5 + LINENUMBER 959 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 961 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$connectTimeout$2 I L3 L5 4 + LOCALVARIABLE $this$connectTimeout_u24lambda_u2431 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE duration Ljava/time/Duration; L0 L7 1 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x11 + public final readTimeout(JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 3 + LDC "unit" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 973 L1 + ALOAD 0 + ASTORE 4 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 5 + L2 + ICONST_0 + ISTORE 6 + L3 + LINENUMBER 974 L3 + ALOAD 5 + LDC "timeout" + LLOAD 1 + ALOAD 3 + INVOKESTATIC okhttp3/internal/Util.checkDuration (Ljava/lang/String;JLjava/util/concurrent/TimeUnit;)I + PUTFIELD okhttp3/OkHttpClient$Builder.readTimeout : I + L4 + LINENUMBER 975 L4 + NOP + L5 + LINENUMBER 973 L5 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 975 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$readTimeout$1 I L3 L5 6 + LOCALVARIABLE $this$readTimeout_u24lambda_u2432 Lokhttp3/OkHttpClient$Builder; L2 L5 5 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE timeout J L0 L7 1 + LOCALVARIABLE unit Ljava/util/concurrent/TimeUnit; L0 L7 3 + MAXSTACK = 5 + MAXLOCALS = 7 + + // access flags 0x11 + public final readTimeout(Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; + @Lorg/codehaus/mojo/animal_sniffer/IgnoreJRERequirement;() // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "duration" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 988 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 989 L3 + ALOAD 3 + ALOAD 1 + INVOKEVIRTUAL java/time/Duration.toMillis ()J + GETSTATIC java/util/concurrent/TimeUnit.MILLISECONDS : Ljava/util/concurrent/TimeUnit; + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.readTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + POP + L4 + LINENUMBER 990 L4 + NOP + L5 + LINENUMBER 988 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 990 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$readTimeout$2 I L3 L5 4 + LOCALVARIABLE $this$readTimeout_u24lambda_u2433 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE duration Ljava/time/Duration; L0 L7 1 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x11 + public final writeTimeout(JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 3 + LDC "unit" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 1001 L1 + ALOAD 0 + ASTORE 4 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 5 + L2 + ICONST_0 + ISTORE 6 + L3 + LINENUMBER 1002 L3 + ALOAD 5 + LDC "timeout" + LLOAD 1 + ALOAD 3 + INVOKESTATIC okhttp3/internal/Util.checkDuration (Ljava/lang/String;JLjava/util/concurrent/TimeUnit;)I + PUTFIELD okhttp3/OkHttpClient$Builder.writeTimeout : I + L4 + LINENUMBER 1003 L4 + NOP + L5 + LINENUMBER 1001 L5 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 1003 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$writeTimeout$1 I L3 L5 6 + LOCALVARIABLE $this$writeTimeout_u24lambda_u2434 Lokhttp3/OkHttpClient$Builder; L2 L5 5 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE timeout J L0 L7 1 + LOCALVARIABLE unit Ljava/util/concurrent/TimeUnit; L0 L7 3 + MAXSTACK = 5 + MAXLOCALS = 7 + + // access flags 0x11 + public final writeTimeout(Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; + @Lorg/codehaus/mojo/animal_sniffer/IgnoreJRERequirement;() // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "duration" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 1015 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 1016 L3 + ALOAD 3 + ALOAD 1 + INVOKEVIRTUAL java/time/Duration.toMillis ()J + GETSTATIC java/util/concurrent/TimeUnit.MILLISECONDS : Ljava/util/concurrent/TimeUnit; + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.writeTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + POP + L4 + LINENUMBER 1017 L4 + NOP + L5 + LINENUMBER 1015 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 1017 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$writeTimeout$2 I L3 L5 4 + LOCALVARIABLE $this$writeTimeout_u24lambda_u2435 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE duration Ljava/time/Duration; L0 L7 1 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x11 + public final pingInterval(JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 2 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1 + L0 + ALOAD 3 + LDC "unit" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 1032 L1 + ALOAD 0 + ASTORE 4 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 5 + L2 + ICONST_0 + ISTORE 6 + L3 + LINENUMBER 1033 L3 + ALOAD 5 + LDC "interval" + LLOAD 1 + ALOAD 3 + INVOKESTATIC okhttp3/internal/Util.checkDuration (Ljava/lang/String;JLjava/util/concurrent/TimeUnit;)I + PUTFIELD okhttp3/OkHttpClient$Builder.pingInterval : I + L4 + LINENUMBER 1034 L4 + NOP + L5 + LINENUMBER 1032 L5 + ALOAD 4 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 1034 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$pingInterval$1 I L3 L5 6 + LOCALVARIABLE $this$pingInterval_u24lambda_u2436 Lokhttp3/OkHttpClient$Builder; L2 L5 5 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE interval J L0 L7 1 + LOCALVARIABLE unit Ljava/util/concurrent/TimeUnit; L0 L7 3 + MAXSTACK = 5 + MAXLOCALS = 7 + + // access flags 0x11 + public final pingInterval(Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; + @Lorg/codehaus/mojo/animal_sniffer/IgnoreJRERequirement;() // invisible + @Lorg/jetbrains/annotations/NotNull;() // invisible + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + L0 + ALOAD 1 + LDC "duration" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 1050 L1 + ALOAD 0 + ASTORE 2 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 3 + L2 + ICONST_0 + ISTORE 4 + L3 + LINENUMBER 1051 L3 + ALOAD 3 + ALOAD 1 + INVOKEVIRTUAL java/time/Duration.toMillis ()J + GETSTATIC java/util/concurrent/TimeUnit.MILLISECONDS : Ljava/util/concurrent/TimeUnit; + INVOKEVIRTUAL okhttp3/OkHttpClient$Builder.pingInterval (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; + POP + L4 + LINENUMBER 1052 L4 + NOP + L5 + LINENUMBER 1050 L5 + ALOAD 2 + CHECKCAST okhttp3/OkHttpClient$Builder + L6 + LINENUMBER 1052 L6 + ARETURN + L7 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$pingInterval$2 I L3 L5 4 + LOCALVARIABLE $this$pingInterval_u24lambda_u2437 Lokhttp3/OkHttpClient$Builder; L2 L5 3 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L7 0 + LOCALVARIABLE duration Ljava/time/Duration; L0 L7 1 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x11 + public final minWebSocketMessageToCompress(J)Lokhttp3/OkHttpClient$Builder; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 1061 L0 + ALOAD 0 + ASTORE 3 + ALOAD 3 + CHECKCAST okhttp3/OkHttpClient$Builder + ASTORE 4 + L1 + ICONST_0 + ISTORE 5 + L2 + LINENUMBER 1062 L2 + LLOAD 1 + LCONST_0 + LCMP + IFLT L3 + ICONST_1 + GOTO L4 + L3 + FRAME FULL [okhttp3/OkHttpClient$Builder J okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ICONST_0 + L4 + FRAME FULL [okhttp3/OkHttpClient$Builder J okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [I] + IFNE L5 + ICONST_0 + ISTORE 6 + L6 + LINENUMBER 1063 L6 + NEW java/lang/StringBuilder + DUP + INVOKESPECIAL java/lang/StringBuilder. ()V + LDC "minWebSocketMessageToCompress must be positive: " + INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; + LLOAD 1 + INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder; + INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; + L7 + LINENUMBER 1062 L7 + ASTORE 6 + NEW java/lang/IllegalArgumentException + DUP + ALOAD 6 + INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String; + INVOKESPECIAL java/lang/IllegalArgumentException. (Ljava/lang/String;)V + ATHROW + L5 + LINENUMBER 1066 L5 + FRAME FULL [okhttp3/OkHttpClient$Builder J okhttp3/OkHttpClient$Builder okhttp3/OkHttpClient$Builder I] [] + ALOAD 4 + LLOAD 1 + PUTFIELD okhttp3/OkHttpClient$Builder.minWebSocketMessageToCompress : J + L8 + LINENUMBER 1067 L8 + NOP + L9 + LINENUMBER 1061 L9 + ALOAD 3 + CHECKCAST okhttp3/OkHttpClient$Builder + L10 + LINENUMBER 1067 L10 + ARETURN + L11 + LOCALVARIABLE $i$a$-require-OkHttpClient$Builder$minWebSocketMessageToCompress$1$1 I L6 L7 6 + LOCALVARIABLE $i$a$-apply-OkHttpClient$Builder$minWebSocketMessageToCompress$1 I L2 L9 5 + LOCALVARIABLE $this$minWebSocketMessageToCompress_u24lambda_u2439 Lokhttp3/OkHttpClient$Builder; L1 L9 4 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L11 0 + LOCALVARIABLE bytes J L0 L11 1 + MAXSTACK = 4 + MAXLOCALS = 7 + + // access flags 0x11 + public final build()Lokhttp3/OkHttpClient; + @Lorg/jetbrains/annotations/NotNull;() // invisible + ALOAD 0 + INVOKESTATIC io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient$Builder._preBuild (Lokhttp3/OkHttpClient$Builder;)V + L0 + LINENUMBER 1069 L0 + NEW okhttp3/OkHttpClient + DUP + ALOAD 0 + INVOKESPECIAL okhttp3/OkHttpClient. (Lokhttp3/OkHttpClient$Builder;)V + ARETURN + L1 + LOCALVARIABLE this Lokhttp3/OkHttpClient$Builder; L0 L1 0 + MAXSTACK = 3 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/ControlObject_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/ControlObject_expected.txt new file mode 100644 index 0000000000..882ca78722 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/ControlObject_expected.txt @@ -0,0 +1,29 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/ControlObject extends androidx/appcompat/app/AppCompatActivity { + + // compiled from: ControlObject.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 10 L0 + ALOAD 0 + INVOKESPECIAL androidx/appcompat/app/AppCompatActivity. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ControlObject; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public processView(Landroid/view/View;)V + L0 + LINENUMBER 13 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ControlObject; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnClickListener_expected.txt new file mode 100644 index 0000000000..c5c98e969f --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnClickListener_expected.txt @@ -0,0 +1,51 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/CustomOnClickListener implements android/view/View$OnClickListener { + + // compiled from: CustomOnClickListener.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0008\u0016\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/CustomOnClickListener;", "Landroid/view/View$OnClickListener;", "()V", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 1 + LDC "view" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 10 L1 + ALOAD 1 + INVOKEVIRTUAL android/view/View.isActivated ()Z + IFEQ L2 + L3 + LINENUMBER 11 L3 + RETURN + L2 + LINENUMBER 13 L2 + FRAME FULL [io/embrace/test/fixtures/CustomOnClickListener android/view/View] [] + RETURN + L4 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomOnClickListener; L0 L4 0 + LOCALVARIABLE view Landroid/view/View; L0 L4 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnLongClickListener_expected.txt new file mode 100644 index 0000000000..593f8d7403 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomOnLongClickListener_expected.txt @@ -0,0 +1,49 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/CustomOnLongClickListener implements android/view/View$OnLongClickListener { + + // compiled from: CustomOnLongClickListener.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0018\u0002\n\u0000\u0008\u0016\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/CustomOnLongClickListener;", "Landroid/view/View$OnLongClickListener;", "()V", "onLongClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 11 L0 + ALOAD 1 + DUP + IFNULL L1 + INVOKEVIRTUAL android/view/View.isActivated ()Z + GOTO L2 + L1 + FRAME FULL [io/embrace/test/fixtures/CustomOnLongClickListener android/view/View] [android/view/View] + POP + ICONST_0 + L2 + FRAME FULL [io/embrace/test/fixtures/CustomOnLongClickListener android/view/View] [I] + IRETURN + L3 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomOnLongClickListener; L0 L3 0 + LOCALVARIABLE view Landroid/view/View; L0 L3 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/CustomWebViewClient_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomWebViewClient_expected.txt new file mode 100644 index 0000000000..a62fc6a188 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/CustomWebViewClient_expected.txt @@ -0,0 +1,48 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/CustomWebViewClient extends android/webkit/WebViewClient { + + // compiled from: CustomWebViewClient.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000$\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0000\u0008\u0016\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J&\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u00062\u0008\u0010\u0007\u001a\u0004\u0018\u00010\u00082\u0008\u0010\u0009\u001a\u0004\u0018\u00010\nH\u0016\u00a8\u0006\u000b"}, d2={"Lio/embrace/test/fixtures/CustomWebViewClient;", "Landroid/webkit/WebViewClient;", "()V", "onPageStarted", "", "view", "Landroid/webkit/WebView;", "url", "", "favicon", "Landroid/graphics/Bitmap;", "embrace-bytecode-instrumentation-tests_release"}) + + // access flags 0x1 + public ()V + L0 + LINENUMBER 7 L0 + ALOAD 0 + INVOKESPECIAL android/webkit/WebViewClient. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomWebViewClient; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onPageStarted(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + // annotable parameter count: 3 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 1 + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 2 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESTATIC io/embrace/android/embracesdk/WebViewClientSwazzledHooks._preOnPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + L0 + LINENUMBER 10 L0 + ALOAD 0 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESPECIAL android/webkit/WebViewClient.onPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + L1 + LINENUMBER 11 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/CustomWebViewClient; L0 L2 0 + LOCALVARIABLE view Landroid/webkit/WebView; L0 L2 1 + LOCALVARIABLE url Ljava/lang/String; L0 L2 2 + LOCALVARIABLE favicon Landroid/graphics/Bitmap; L0 L2 3 + MAXSTACK = 4 + MAXLOCALS = 4 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedCustomWebViewClient_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedCustomWebViewClient_expected.txt new file mode 100644 index 0000000000..e528f1d770 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedCustomWebViewClient_expected.txt @@ -0,0 +1,48 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/ExtendedCustomWebViewClient extends io/embrace/test/fixtures/CustomWebViewClient { + + // compiled from: ExtendedCustomWebViewClient.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000$\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J&\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u00062\u0008\u0010\u0007\u001a\u0004\u0018\u00010\u00082\u0008\u0010\u0009\u001a\u0004\u0018\u00010\nH\u0016\u00a8\u0006\u000b"}, d2={"Lio/embrace/test/fixtures/ExtendedCustomWebViewClient;", "Lio/embrace/test/fixtures/CustomWebViewClient;", "()V", "onPageStarted", "", "view", "Landroid/webkit/WebView;", "url", "", "favicon", "Landroid/graphics/Bitmap;", "embrace-bytecode-instrumentation-tests_release"}) + + // access flags 0x1 + public ()V + L0 + LINENUMBER 6 L0 + ALOAD 0 + INVOKESPECIAL io/embrace/test/fixtures/CustomWebViewClient. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedCustomWebViewClient; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onPageStarted(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + // annotable parameter count: 3 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 1 + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 2 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESTATIC io/embrace/android/embracesdk/WebViewClientSwazzledHooks._preOnPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + L0 + LINENUMBER 8 L0 + ALOAD 0 + ALOAD 1 + LDC "http://google.com" + ALOAD 3 + INVOKESPECIAL io/embrace/test/fixtures/CustomWebViewClient.onPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + L1 + LINENUMBER 9 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedCustomWebViewClient; L0 L2 0 + LOCALVARIABLE view Landroid/webkit/WebView; L0 L2 1 + LOCALVARIABLE url Ljava/lang/String; L0 L2 2 + LOCALVARIABLE favicon Landroid/graphics/Bitmap; L0 L2 3 + MAXSTACK = 4 + MAXLOCALS = 4 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnClickListener_expected.txt new file mode 100644 index 0000000000..47aa4f7241 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnClickListener_expected.txt @@ -0,0 +1,67 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/ExtendedOnClickListener extends io/embrace/test/fixtures/CustomOnClickListener { + + // compiled from: ExtendedOnClickListener.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 11 L0 + ALOAD 0 + INVOKESPECIAL io/embrace/test/fixtures/CustomOnClickListener. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 16 L0 + ALOAD 0 + INVOKEVIRTUAL io/embrace/test/fixtures/ExtendedOnClickListener.doSomething ()I + POP + L1 + LINENUMBER 17 L1 + ALOAD 0 + ALOAD 1 + INVOKESPECIAL io/embrace/test/fixtures/CustomOnClickListener.onClick (Landroid/view/View;)V + L2 + LINENUMBER 19 L2 + ALOAD 1 + INVOKEVIRTUAL android/view/View.isEnabled ()Z + IFEQ L3 + L4 + LINENUMBER 20 L4 + LDC "EmbraceTest" + LDC "Clicked a button" + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L3 + LINENUMBER 22 L3 + FRAME FULL [io/embrace/test/fixtures/ExtendedOnClickListener android/view/View] [] + RETURN + L5 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnClickListener; L0 L5 0 + LOCALVARIABLE view Landroid/view/View; L0 L5 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x2 + private doSomething()I + L0 + LINENUMBER 25 L0 + LDC 1088681 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnLongClickListener_expected.txt new file mode 100644 index 0000000000..ab59254080 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/ExtendedOnLongClickListener_expected.txt @@ -0,0 +1,60 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/ExtendedOnLongClickListener extends io/embrace/test/fixtures/CustomOnLongClickListener { + + // compiled from: ExtendedOnLongClickListener.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 13 L0 + ALOAD 0 + INVOKESPECIAL io/embrace/test/fixtures/CustomOnLongClickListener. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + // annotable parameter count: 1 (invisible) + @Landroidx/annotation/Nullable;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 17 L0 + ALOAD 1 + INVOKEVIRTUAL android/view/View.isEnabled ()Z + IFEQ L1 + L2 + LINENUMBER 18 L2 + LDC "EmbraceTest" + LDC "Clicked a button" + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L1 + LINENUMBER 20 L1 + FRAME FULL [io/embrace/test/fixtures/ExtendedOnLongClickListener android/view/View] [] + ALOAD 0 + ALOAD 1 + INVOKESPECIAL io/embrace/test/fixtures/CustomOnLongClickListener.onLongClick (Landroid/view/View;)Z + IRETURN + L3 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnLongClickListener; L0 L3 0 + LOCALVARIABLE view Landroid/view/View; L0 L3 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x2 + private doSomething()I + L0 + LINENUMBER 24 L0 + LDC 1088681 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/ExtendedOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/FragmentOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/FragmentOnClickListener_expected.txt new file mode 100644 index 0000000000..1c336d93a5 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/FragmentOnClickListener_expected.txt @@ -0,0 +1,44 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/FragmentOnClickListener extends androidx/fragment/app/Fragment implements android/view/View$OnClickListener { + + // compiled from: FragmentOnClickListener.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u00012\u00020\u0002B\u0005\u00a2\u0006\u0002\u0010\u0003J\u0010\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u0007H\u0016\u00a8\u0006\u0008"}, d2={"Lio/embrace/test/fixtures/FragmentOnClickListener;", "Landroidx/fragment/app/Fragment;", "Landroid/view/View$OnClickListener;", "()V", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x609 + public static abstract INNERCLASS androidx/lifecycle/ViewModelProvider$Factory androidx/lifecycle/ViewModelProvider Factory + + // access flags 0x1 + public ()V + L0 + LINENUMBER 9 L0 + ALOAD 0 + INVOKESPECIAL androidx/fragment/app/Fragment. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/FragmentOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 1 + LDC "view" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 11 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/FragmentOnClickListener; L0 L2 0 + LOCALVARIABLE view Landroid/view/View; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/JavaAnonOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaAnonOnClickListener_expected.txt new file mode 100644 index 0000000000..5ffa8d6ff0 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaAnonOnClickListener_expected.txt @@ -0,0 +1,51 @@ +// class version 55.0 (55) +// access flags 0x20 +class io/embrace/test/fixtures/JavaAnonOnClickListener$1 implements android/view/View$OnClickListener { + + // compiled from: JavaAnonOnClickListener.java + NESTHOST io/embrace/test/fixtures/JavaAnonOnClickListener + OUTERCLASS io/embrace/test/fixtures/JavaAnonOnClickListener onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + // access flags 0x0 + INNERCLASS io/embrace/test/fixtures/JavaAnonOnClickListener$1 null null + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/JavaAnonOnClickListener; this$0 + + // access flags 0x0 + (Lio/embrace/test/fixtures/JavaAnonOnClickListener;)V + L0 + LINENUMBER 22 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/JavaAnonOnClickListener$1.this$0 : Lio/embrace/test/fixtures/JavaAnonOnClickListener; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaAnonOnClickListener$1; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/JavaAnonOnClickListener; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 25 L0 + LDC "Embrace" + LDC "test " + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L1 + LINENUMBER 26 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaAnonOnClickListener$1; L0 L2 0 + LOCALVARIABLE lambdaView Landroid/view/View; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/JavaInnerListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaInnerListener_expected.txt new file mode 100644 index 0000000000..2a91b24faf --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaInnerListener_expected.txt @@ -0,0 +1,44 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/JavaNested$JavaInnerListener implements android/view/View$OnClickListener { + + // compiled from: JavaNested.java + NESTHOST io/embrace/test/fixtures/JavaNested + // access flags 0x1 + public INNERCLASS io/embrace/test/fixtures/JavaNested$JavaInnerListener io/embrace/test/fixtures/JavaNested JavaInnerListener + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/JavaNested; this$0 + + // access flags 0x1 + public (Lio/embrace/test/fixtures/JavaNested;)V + L0 + LINENUMBER 11 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/JavaNested$JavaInnerListener.this$0 : Lio/embrace/test/fixtures/JavaNested; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaNested$JavaInnerListener; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/JavaNested; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 15 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaNested$JavaInnerListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnClickListener_expected.txt new file mode 100644 index 0000000000..015abdecb2 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnClickListener_expected.txt @@ -0,0 +1,84 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/JavaLambdaOnClickListener extends androidx/fragment/app/Fragment { + + // compiled from: JavaLambdaOnClickListener.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x19 + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + + // access flags 0x1 + public ()V + L0 + LINENUMBER 15 L0 + ALOAD 0 + INVOKESPECIAL androidx/fragment/app/Fragment. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaLambdaOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + // annotable parameter count: 3 (invisible) + @Landroidx/annotation/NonNull;() // invisible, parameter 0 + L0 + LINENUMBER 19 L0 + ALOAD 0 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESPECIAL androidx/fragment/app/Fragment.onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + ASTORE 4 + L1 + LINENUMBER 20 L1 + ALOAD 4 + IFNULL L2 + L3 + LINENUMBER 21 L3 + ALOAD 4 + INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [ + // handle kind 0x6 : INVOKESTATIC + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + // arguments: + (Landroid/view/View;)V, + // handle kind 0x6 : INVOKESTATIC + io/embrace/test/fixtures/JavaLambdaOnClickListener.lambda$onCreateView$0(Landroid/view/View;)V, + (Landroid/view/View;)V + ] + INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V + L2 + LINENUMBER 25 L2 + FRAME FULL [io/embrace/test/fixtures/JavaLambdaOnClickListener android/view/LayoutInflater android/view/ViewGroup android/os/Bundle android/view/View] [] + ALOAD 4 + ARETURN + L4 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaLambdaOnClickListener; L0 L4 0 + LOCALVARIABLE inflater Landroid/view/LayoutInflater; L0 L4 1 + LOCALVARIABLE container Landroid/view/ViewGroup; L0 L4 2 + LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L4 3 + LOCALVARIABLE view Landroid/view/View; L1 L4 4 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x100A + private static synthetic lambda$onCreateView$0(Landroid/view/View;)V + ACONST_NULL + ALOAD 0 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 22 L0 + LDC "Embrace" + LDC "test" + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L1 + LINENUMBER 23 L1 + RETURN + L2 + LOCALVARIABLE lambdaView Landroid/view/View; L0 L2 0 + MAXSTACK = 2 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnLongClickListener_expected.txt new file mode 100644 index 0000000000..4dea0c5f0a --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaLambdaOnLongClickListener_expected.txt @@ -0,0 +1,85 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/JavaLambdaOnLongClickListener extends androidx/fragment/app/Fragment { + + // compiled from: JavaLambdaOnLongClickListener.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + // access flags 0x19 + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + + // access flags 0x1 + public ()V + L0 + LINENUMBER 15 L0 + ALOAD 0 + INVOKESPECIAL androidx/fragment/app/Fragment. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaLambdaOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + // annotable parameter count: 3 (invisible) + @Landroidx/annotation/NonNull;() // invisible, parameter 0 + L0 + LINENUMBER 19 L0 + ALOAD 0 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESPECIAL androidx/fragment/app/Fragment.onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + ASTORE 4 + L1 + LINENUMBER 20 L1 + ALOAD 4 + IFNULL L2 + L3 + LINENUMBER 21 L3 + ALOAD 4 + INVOKEDYNAMIC onLongClick()Landroid/view/View$OnLongClickListener; [ + // handle kind 0x6 : INVOKESTATIC + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + // arguments: + (Landroid/view/View;)Z, + // handle kind 0x6 : INVOKESTATIC + io/embrace/test/fixtures/JavaLambdaOnLongClickListener.lambda$onCreateView$0(Landroid/view/View;)Z, + (Landroid/view/View;)Z + ] + INVOKEVIRTUAL android/view/View.setOnLongClickListener (Landroid/view/View$OnLongClickListener;)V + L2 + LINENUMBER 26 L2 + FRAME FULL [io/embrace/test/fixtures/JavaLambdaOnLongClickListener android/view/LayoutInflater android/view/ViewGroup android/os/Bundle android/view/View] [] + ALOAD 4 + ARETURN + L4 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaLambdaOnLongClickListener; L0 L4 0 + LOCALVARIABLE inflater Landroid/view/LayoutInflater; L0 L4 1 + LOCALVARIABLE container Landroid/view/ViewGroup; L0 L4 2 + LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L4 3 + LOCALVARIABLE view Landroid/view/View; L1 L4 4 + MAXSTACK = 4 + MAXLOCALS = 5 + + // access flags 0x100A + private static synthetic lambda$onCreateView$0(Landroid/view/View;)Z + ACONST_NULL + ALOAD 0 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 22 L0 + LDC "Embrace" + LDC "test" + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L1 + LINENUMBER 23 L1 + ICONST_1 + IRETURN + L2 + LOCALVARIABLE lambdaView Landroid/view/View; L0 L2 0 + MAXSTACK = 2 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/JavaStaticListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaStaticListener_expected.txt new file mode 100644 index 0000000000..d515cafae0 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/JavaStaticListener_expected.txt @@ -0,0 +1,37 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/JavaNested$JavaStaticListener implements android/view/View$OnClickListener { + + // compiled from: JavaNested.java + NESTHOST io/embrace/test/fixtures/JavaNested + // access flags 0x9 + public static INNERCLASS io/embrace/test/fixtures/JavaNested$JavaStaticListener io/embrace/test/fixtures/JavaNested JavaStaticListener + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 18 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaNested$JavaStaticListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 22 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/JavaNested$JavaStaticListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinInnerListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinInnerListener_expected.txt new file mode 100644 index 0000000000..690d89a6f5 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinInnerListener_expected.txt @@ -0,0 +1,53 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinNested$KotlinInnerListener implements android/view/View$OnClickListener { + + // compiled from: KotlinNested.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0008\u0086\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/KotlinNested$KotlinInnerListener;", "Landroid/view/View$OnClickListener;", "(Lio/embrace/test/fixtures/KotlinNested;)V", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x11 + public final INNERCLASS io/embrace/test/fixtures/KotlinNested$KotlinInnerListener io/embrace/test/fixtures/KotlinNested KotlinInnerListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/KotlinNested; this$0 + + // access flags 0x1 + // signature ()V + // declaration: void () + public (Lio/embrace/test/fixtures/KotlinNested;)V + L0 + LINENUMBER 9 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/KotlinNested$KotlinInnerListener.this$0 : Lio/embrace/test/fixtures/KotlinNested; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNested$KotlinInnerListener; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/KotlinNested; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 1 + LDC "view" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 10 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNested$KotlinInnerListener; L0 L2 0 + LOCALVARIABLE view Landroid/view/View; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnClickListener_expected.txt new file mode 100644 index 0000000000..621424d627 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnClickListener_expected.txt @@ -0,0 +1,39 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1 implements android/view/View$OnClickListener { + + // compiled from: KotlinObjectOnClickListener.kt + OUTERCLASS io/embrace/test/fixtures/KotlinObjectOnClickListener onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + + @Lkotlin/Metadata;(mv={1, 6, 0}, k=1, xi=48, d1={"\u0000\u0017\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000*\u0001\u0000\u0008\n\u0018\u00002\u00020\u0001J\u0012\u0010\u0002\u001a\u00020\u00032\u0008\u0010\u0004\u001a\u0004\u0018\u00010\u0005H\u0016\u00a8\u0006\u0006"}, d2={"io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1", "Landroid/view/View$OnClickListener;", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x19 + public final static INNERCLASS io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1 null null + + // access flags 0x0 + ()V + L0 + LINENUMBER 17 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 20 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnLongClickListener_expected.txt new file mode 100644 index 0000000000..7e70f9dcae --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinLambdaOnLongClickListener_expected.txt @@ -0,0 +1,47 @@ +// class version 55.0 (55) +// access flags 0x30 +final class io/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1 implements android/view/View$OnLongClickListener { + + // compiled from: KotlinLambdaOnLongClickListener.kt + OUTERCLASS io/embrace/test/fixtures/KotlinLambdaOnLongClickListener onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + + @Lkotlin/Metadata;(mv={1, 4, 2}, bv={1, 0, 3}, k=3, d1={"\u0000\u0010\n\u0000\n\u0002\u0010\u000b\n\u0000\n\u0002\u0018\u0002\n\u0002\u0008\u0002\u0010\u0000\u001a\u00020\u00012\u000e\u0010\u0002\u001a\n \u0004*\u0004\u0018\u00010\u00030\u0003H\n\u00a2\u0006\u0002\u0008\u0005"}, d2={"", "", "it", "Landroid/view/View;", "kotlin.jvm.PlatformType", "onLongClick"}) + // access flags 0x18 + final static INNERCLASS io/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1 null null + + // access flags 0x19 + public final static Lio/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1; INSTANCE + + // access flags 0x11 + public final onLongClick(Landroid/view/View;)Z + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 18 L0 + ICONST_0 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1; L0 L1 0 + LOCALVARIABLE it Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 + + // access flags 0x0 + ()V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x8 + static ()V + NEW io/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1 + DUP + INVOKESPECIAL io/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1. ()V + PUTSTATIC io/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1.INSTANCE : Lio/embrace/test/fixtures/KotlinLambdaOnLongClickListener$onCreateView$1; + RETURN + MAXSTACK = 2 + MAXLOCALS = 0 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRef2OnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRef2OnClickListener_expected.txt new file mode 100644 index 0000000000..3f1b43ad6d --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRef2OnClickListener_expected.txt @@ -0,0 +1,43 @@ +// class version 55.0 (55) +// access flags 0x30 +final class io/embrace/test/fixtures/KotlinMethodRef2OnClickListener$sam$android_view_View_OnClickListener$0 implements android/view/View$OnClickListener { + + // compiled from: KotlinMethodRef2OnClickListener.kt + + @Lkotlin/Metadata;(mv={1, 4, 2}, bv={1, 0, 3}, k=3) + + // access flags 0x1012 + private final synthetic Lkotlin/jvm/functions/Function1; function + + // access flags 0x0 + (Lkotlin/jvm/functions/Function1;)V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/KotlinMethodRef2OnClickListener$sam$android_view_View_OnClickListener$0.function : Lkotlin/jvm/functions/Function1; + RETURN + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1011 + public final synthetic onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 0 + GETFIELD io/embrace/test/fixtures/KotlinMethodRef2OnClickListener$sam$android_view_View_OnClickListener$0.function : Lkotlin/jvm/functions/Function1; + ALOAD 1 + INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object; (itf) + DUP + LDC "invoke(...)" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V + POP + RETURN + L1 + LOCALVARIABLE this Landroid/view/View$OnClickListener; L0 L1 0 + LOCALVARIABLE p0 Landroid/view/View; L0 L1 1 + MAXSTACK = 3 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnClickListener_expected.txt new file mode 100644 index 0000000000..6faa88a798 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnClickListener_expected.txt @@ -0,0 +1,43 @@ +// class version 55.0 (55) +// access flags 0x30 +final class io/embrace/test/fixtures/KotlinMethodRefOnClickListener$sam$android_view_View_OnClickListener$0 implements android/view/View$OnClickListener { + + // compiled from: KotlinMethodRefOnClickListener.kt + + @Lkotlin/Metadata;(mv={1, 4, 2}, bv={1, 0, 3}, k=3) + + // access flags 0x1012 + private final synthetic Lkotlin/jvm/functions/Function1; function + + // access flags 0x0 + (Lkotlin/jvm/functions/Function1;)V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/KotlinMethodRefOnClickListener$sam$android_view_View_OnClickListener$0.function : Lkotlin/jvm/functions/Function1; + RETURN + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1011 + public final synthetic onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 0 + GETFIELD io/embrace/test/fixtures/KotlinMethodRefOnClickListener$sam$android_view_View_OnClickListener$0.function : Lkotlin/jvm/functions/Function1; + ALOAD 1 + INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object; (itf) + DUP + LDC "invoke(...)" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V + POP + RETURN + L1 + LOCALVARIABLE this Landroid/view/View$OnClickListener; L0 L1 0 + LOCALVARIABLE p0 Landroid/view/View; L0 L1 1 + MAXSTACK = 3 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnLongClickListener_expected.txt new file mode 100644 index 0000000000..7d89a000c6 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinMethodRefOnLongClickListener_expected.txt @@ -0,0 +1,44 @@ +// class version 55.0 (55) +// access flags 0x30 +final class io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener$sam$android_view_View_OnLongClickListener$0 implements android/view/View$OnLongClickListener { + + // compiled from: KotlinMethodRefOnLongClickListener.kt + + @Lkotlin/Metadata;(mv={1, 4, 2}, bv={1, 0, 3}, k=3) + + // access flags 0x1012 + private final synthetic Lkotlin/jvm/functions/Function1; function + + // access flags 0x0 + (Lkotlin/jvm/functions/Function1;)V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener$sam$android_view_View_OnLongClickListener$0.function : Lkotlin/jvm/functions/Function1; + RETURN + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1011 + public final synthetic onLongClick(Landroid/view/View;)Z + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + ALOAD 0 + GETFIELD io/embrace/test/fixtures/KotlinMethodRefOnLongClickListener$sam$android_view_View_OnLongClickListener$0.function : Lkotlin/jvm/functions/Function1; + ALOAD 1 + INVOKEINTERFACE kotlin/jvm/functions/Function1.invoke (Ljava/lang/Object;)Ljava/lang/Object; (itf) + DUP + LDC "invoke(...)" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V + CHECKCAST java/lang/Boolean + INVOKEVIRTUAL java/lang/Boolean.booleanValue ()Z + IRETURN + L1 + LOCALVARIABLE this Landroid/view/View$OnLongClickListener; L0 L1 0 + LOCALVARIABLE p0 Landroid/view/View; L0 L1 1 + MAXSTACK = 3 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinObjectOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinObjectOnClickListener_expected.txt new file mode 100644 index 0000000000..6163eca7db --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinObjectOnClickListener_expected.txt @@ -0,0 +1,39 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1 implements android/view/View$OnClickListener { + + // compiled from: KotlinObjectOnClickListener.kt + OUTERCLASS io/embrace/test/fixtures/KotlinObjectOnClickListener onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0017\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000*\u0001\u0000\u0008\n\u0018\u00002\u00020\u0001J\u0012\u0010\u0002\u001a\u00020\u00032\u0008\u0010\u0004\u001a\u0004\u0018\u00010\u0005H\u0016\u00a8\u0006\u0006"}, d2={"io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1", "Landroid/view/View$OnClickListener;", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x19 + public final static INNERCLASS io/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1 null null + + // access flags 0x0 + ()V + L0 + LINENUMBER 17 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 19 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinObjectOnClickListener$onCreateView$1; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinStaticListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinStaticListener_expected.txt new file mode 100644 index 0000000000..b6b0fbd4e7 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/KotlinStaticListener_expected.txt @@ -0,0 +1,44 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinNested$KotlinStaticListener implements android/view/View$OnClickListener { + + // compiled from: KotlinNested.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/KotlinNested$KotlinStaticListener;", "Landroid/view/View$OnClickListener;", "()V", "onClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x19 + public final static INNERCLASS io/embrace/test/fixtures/KotlinNested$KotlinStaticListener io/embrace/test/fixtures/KotlinNested KotlinStaticListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 13 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNested$KotlinStaticListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + ALOAD 1 + LDC "view" + INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V + L1 + LINENUMBER 14 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNested$KotlinStaticListener; L0 L2 0 + LOCALVARIABLE view Landroid/view/View; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/MethodReturnValueVisitorObj_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/MethodReturnValueVisitorObj_expected.txt new file mode 100644 index 0000000000..b5a2142be9 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/MethodReturnValueVisitorObj_expected.txt @@ -0,0 +1,125 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/MethodReturnValueVisitorObj { + + // compiled from: MethodReturnValueVisitorObj.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u00000\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u0008\n\u0000\n\u0002\u0010 \n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0009\n\u0000\n\u0002\u0010$\n\u0002\u0008\u0002\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0006\u0010\u0005\u001a\u00020\u0006J\u000c\u0010\u0007\u001a\u0008\u0012\u0004\u0012\u00020\u00090\u0008J\u0006\u0010\n\u001a\u00020\u000bJ\u0012\u0010\u000c\u001a\u000e\u0012\u0004\u0012\u00020\u0009\u0012\u0004\u0012\u00020\u00060\rJ\u0006\u0010\u000e\u001a\u00020\u0009\u00a8\u0006\u000f"}, d2={"Lio/embrace/test/fixtures/MethodReturnValueVisitorObj;", "", "()V", "getSomeBool", "", "getSomeInt", "", "getSomeList", "", "", "getSomeLong", "", "getSomeMap", "", "getSomeStr", "embrace-bytecode-instrumentation-tests_release"}) + + // access flags 0x1 + public ()V + L0 + LINENUMBER 3 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + L1 + LINENUMBER 4 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L2 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final getSomeBool()Z + L0 + LINENUMBER 5 L0 + ICONST_0 + ICONST_1 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final getSomeInt()I + L0 + LINENUMBER 6 L0 + BIPUSH 100 + LDC 520 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + public final getSomeLong()J + L0 + LINENUMBER 7 L0 + LDC 2 + LDC 150900202020202 + LRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 2 + MAXLOCALS = 1 + + // access flags 0x11 + public final getSomeStr()Ljava/lang/String; + @Lorg/jetbrains/annotations/NotNull;() // invisible + L0 + LINENUMBER 8 L0 + LDC "Hi" + LDC "Hello world! I'm a string." + ARETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + // signature ()Ljava/util/List; + // declaration: java.util.List getSomeList() + public final getSomeList()Ljava/util/List; + @Lorg/jetbrains/annotations/NotNull;() // invisible + NEW java/util/ArrayList + DUP + INVOKESPECIAL java/util/ArrayList. ()V + DUP + LDC "adam aardvark" + INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z + POP + DUP + LDC "bob banana" + INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z + POP + ARETURN + L0 + LINENUMBER 9 L0 + INVOKESTATIC kotlin/collections/CollectionsKt.emptyList ()Ljava/util/List; + ARETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x11 + // signature ()Ljava/util/Map; + // declaration: java.util.Map getSomeMap() + public final getSomeMap()Ljava/util/Map; + @Lorg/jetbrains/annotations/NotNull;() // invisible + NEW java/util/HashMap + DUP + INVOKESPECIAL java/util/HashMap. ()V + DUP + LDC "adam" + LDC "1" + INVOKEVIRTUAL java/util/HashMap.put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + POP + DUP + LDC "bob" + LDC "2" + INVOKEVIRTUAL java/util/HashMap.put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + POP + ARETURN + L0 + LINENUMBER 10 L0 + INVOKESTATIC kotlin/collections/MapsKt.emptyMap ()Ljava/util/Map; + ARETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MethodReturnValueVisitorObj; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnClickListener_expected.txt new file mode 100644 index 0000000000..1832e100dd --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnClickListener_expected.txt @@ -0,0 +1,42 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/MissingInterfaceOnClickListener { + + // compiled from: MissingInterfaceOnClickListener.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 11 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnClickListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 + + // access flags 0x1 + public onClick()V + L0 + LINENUMBER 14 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnClickListener; L0 L1 0 + MAXSTACK = 0 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnLongClickListener_expected.txt new file mode 100644 index 0000000000..2b608e943f --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingInterfaceOnLongClickListener_expected.txt @@ -0,0 +1,44 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/MissingInterfaceOnLongClickListener { + + // compiled from: MissingInterfaceOnLongClickListener.java + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 10 L0 + ICONST_1 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnLongClickListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 + + // access flags 0x1 + public onLongClick()Z + L0 + LINENUMBER 14 L0 + ICONST_1 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingInterfaceOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnClickListener_expected.txt new file mode 100644 index 0000000000..1ce673c9a2 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnClickListener_expected.txt @@ -0,0 +1,34 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/MissingOverrideOnClickListener implements android/view/View$OnClickListener { + + // compiled from: MissingOverrideOnClickListener.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingOverrideOnClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 11 L0 + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingOverrideOnClickListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 0 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnLongClickListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnLongClickListener_expected.txt new file mode 100644 index 0000000000..4e048fd4dd --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/MissingOverrideOnLongClickListener_expected.txt @@ -0,0 +1,35 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/MissingOverrideOnLongClickListener implements android/view/View$OnLongClickListener { + + // compiled from: MissingOverrideOnLongClickListener.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingOverrideOnLongClickListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 10 L0 + ICONST_0 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/MissingOverrideOnLongClickListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/NetworkDispatcher_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/NetworkDispatcher_expected.txt new file mode 100644 index 0000000000..4e6c443ac3 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/NetworkDispatcher_expected.txt @@ -0,0 +1,491 @@ +// class version 51.0 (51) +// access flags 0x21 +public class com/android/volley/NetworkDispatcher extends java/lang/Thread { + + // compiled from: NetworkDispatcher.java + // access flags 0x9 + public static INNERCLASS android/os/Build$VERSION_CODES android/os/Build VERSION_CODES + // access flags 0x2609 + public static abstract INNERCLASS com/android/volley/RequestQueue$RequestEvent com/android/volley/RequestQueue RequestEvent + // access flags 0x9 + public static INNERCLASS android/os/Build$VERSION android/os/Build VERSION + // access flags 0x9 + public static INNERCLASS com/android/volley/Cache$Entry com/android/volley/Cache Entry + + // access flags 0x12 + // signature Ljava/util/concurrent/BlockingQueue;>; + // declaration: mQueue extends java.util.concurrent.BlockingQueue> + private final Ljava/util/concurrent/BlockingQueue; mQueue + + // access flags 0x12 + private final Lcom/android/volley/Network; mNetwork + + // access flags 0x12 + private final Lcom/android/volley/Cache; mCache + + // access flags 0x12 + private final Lcom/android/volley/ResponseDelivery; mDelivery + + // access flags 0x42 + private volatile Z mQuit + + // access flags 0x1 + // signature (Ljava/util/concurrent/BlockingQueue;>;Lcom/android/volley/Network;Lcom/android/volley/Cache;Lcom/android/volley/ResponseDelivery;)V + // declaration: void (java.util.concurrent.BlockingQueue>, com.android.volley.Network, com.android.volley.Cache, com.android.volley.ResponseDelivery) + public (Ljava/util/concurrent/BlockingQueue;Lcom/android/volley/Network;Lcom/android/volley/Cache;Lcom/android/volley/ResponseDelivery;)V + L0 + LINENUMBER 61 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Thread. ()V + L1 + LINENUMBER 46 L1 + ALOAD 0 + ICONST_0 + PUTFIELD com/android/volley/NetworkDispatcher.mQuit : Z + L2 + LINENUMBER 62 L2 + ALOAD 0 + ALOAD 1 + PUTFIELD com/android/volley/NetworkDispatcher.mQueue : Ljava/util/concurrent/BlockingQueue; + L3 + LINENUMBER 63 L3 + ALOAD 0 + ALOAD 2 + PUTFIELD com/android/volley/NetworkDispatcher.mNetwork : Lcom/android/volley/Network; + L4 + LINENUMBER 64 L4 + ALOAD 0 + ALOAD 3 + PUTFIELD com/android/volley/NetworkDispatcher.mCache : Lcom/android/volley/Cache; + L5 + LINENUMBER 65 L5 + ALOAD 0 + ALOAD 4 + PUTFIELD com/android/volley/NetworkDispatcher.mDelivery : Lcom/android/volley/ResponseDelivery; + L6 + LINENUMBER 66 L6 + RETURN + L7 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L0 L7 0 + LOCALVARIABLE queue Ljava/util/concurrent/BlockingQueue; L0 L7 1 + // signature Ljava/util/concurrent/BlockingQueue;>; + // declaration: queue extends java.util.concurrent.BlockingQueue> + LOCALVARIABLE network Lcom/android/volley/Network; L0 L7 2 + LOCALVARIABLE cache Lcom/android/volley/Cache; L0 L7 3 + LOCALVARIABLE delivery Lcom/android/volley/ResponseDelivery; L0 L7 4 + MAXSTACK = 2 + MAXLOCALS = 5 + + // access flags 0x1 + public quit()V + L0 + LINENUMBER 73 L0 + ALOAD 0 + ICONST_1 + PUTFIELD com/android/volley/NetworkDispatcher.mQuit : Z + L1 + LINENUMBER 74 L1 + ALOAD 0 + INVOKEVIRTUAL com/android/volley/NetworkDispatcher.interrupt ()V + L2 + LINENUMBER 75 L2 + RETURN + L3 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L0 L3 0 + MAXSTACK = 2 + MAXLOCALS = 1 + + // access flags 0x2 + // signature (Lcom/android/volley/Request<*>;)V + // declaration: void addTrafficStatsTag(com.android.volley.Request) + private addTrafficStatsTag(Lcom/android/volley/Request;)V + @Landroid/annotation/TargetApi;(value=14) // invisible + L0 + LINENUMBER 80 L0 + GETSTATIC android/os/Build$VERSION.SDK_INT : I + BIPUSH 14 + IF_ICMPLT L1 + L2 + LINENUMBER 81 L2 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.getTrafficStatsTag ()I + INVOKESTATIC android/net/TrafficStats.setThreadStatsTag (I)V + L1 + LINENUMBER 83 L1 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request] [] + RETURN + L3 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L0 L3 0 + LOCALVARIABLE request Lcom/android/volley/Request; L0 L3 1 + // signature Lcom/android/volley/Request<*>; + // declaration: request extends com.android.volley.Request + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public run()V + TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException + L3 + LINENUMBER 87 L3 + BIPUSH 10 + INVOKESTATIC android/os/Process.setThreadPriority (I)V + L0 + LINENUMBER 90 L0 + FRAME FULL [com/android/volley/NetworkDispatcher] [] + ALOAD 0 + INVOKESPECIAL com/android/volley/NetworkDispatcher.processRequest ()V + L1 + LINENUMBER 100 L1 + GOTO L0 + L2 + LINENUMBER 91 L2 + FRAME FULL [com/android/volley/NetworkDispatcher] [java/lang/InterruptedException] + ASTORE 1 + L4 + LINENUMBER 93 L4 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mQuit : Z + IFEQ L5 + L6 + LINENUMBER 94 L6 + INVOKESTATIC java/lang/Thread.currentThread ()Ljava/lang/Thread; + INVOKEVIRTUAL java/lang/Thread.interrupt ()V + L7 + LINENUMBER 95 L7 + RETURN + L5 + LINENUMBER 97 L5 + FRAME FULL [com/android/volley/NetworkDispatcher java/lang/InterruptedException] [] + LDC "Ignoring spurious interrupt of NetworkDispatcher thread; use quit() to terminate it" + ICONST_0 + ANEWARRAY java/lang/Object + INVOKESTATIC com/android/volley/VolleyLog.e (Ljava/lang/String;[Ljava/lang/Object;)V + L8 + LINENUMBER 100 L8 + GOTO L0 + L9 + LOCALVARIABLE e Ljava/lang/InterruptedException; L4 L8 1 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L3 L9 0 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x2 + private processRequest()V throws java/lang/InterruptedException + L0 + LINENUMBER 110 L0 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mQueue : Ljava/util/concurrent/BlockingQueue; + INVOKEINTERFACE java/util/concurrent/BlockingQueue.take ()Ljava/lang/Object; (itf) + CHECKCAST com/android/volley/Request + ASTORE 1 + L1 + LINENUMBER 111 L1 + ALOAD 0 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/NetworkDispatcher.processRequest (Lcom/android/volley/Request;)V + L2 + LINENUMBER 112 L2 + RETURN + L3 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L0 L3 0 + LOCALVARIABLE request Lcom/android/volley/Request; L1 L3 1 + // signature Lcom/android/volley/Request<*>; + // declaration: request extends com.android.volley.Request + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x0 + // signature (Lcom/android/volley/Request<*>;)V + // declaration: void processRequest(com.android.volley.Request) + processRequest(Lcom/android/volley/Request;)V + @Landroidx/annotation/VisibleForTesting;() // invisible + TRYCATCHBLOCK L0 L1 L2 com/android/volley/VolleyError + TRYCATCHBLOCK L3 L4 L2 com/android/volley/VolleyError + TRYCATCHBLOCK L5 L6 L2 com/android/volley/VolleyError + TRYCATCHBLOCK L0 L1 L7 java/lang/Exception + TRYCATCHBLOCK L3 L4 L7 java/lang/Exception + TRYCATCHBLOCK L5 L6 L7 java/lang/Exception + TRYCATCHBLOCK L0 L1 L8 null + TRYCATCHBLOCK L3 L4 L8 null + TRYCATCHBLOCK L5 L6 L8 null + TRYCATCHBLOCK L2 L9 L8 null + TRYCATCHBLOCK L7 L10 L8 null + TRYCATCHBLOCK L8 L11 L8 null + L12 + LINENUMBER 116 L12 + INVOKESTATIC android/os/SystemClock.elapsedRealtime ()J + LSTORE 2 + L13 + LINENUMBER 117 L13 + ALOAD 1 + ICONST_3 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L0 + LINENUMBER 119 L0 + ALOAD 1 + LDC "network-queue-take" + INVOKEVIRTUAL com/android/volley/Request.addMarker (Ljava/lang/String;)V + L14 + LINENUMBER 123 L14 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.isCanceled ()Z + IFEQ L3 + L15 + LINENUMBER 124 L15 + ALOAD 1 + LDC "network-discard-cancelled" + INVOKEVIRTUAL com/android/volley/Request.finish (Ljava/lang/String;)V + L16 + LINENUMBER 125 L16 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.notifyListenerResponseNotUsable ()V + L1 + LINENUMBER 169 L1 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L17 + LINENUMBER 126 L17 + RETURN + L3 + LINENUMBER 129 L3 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J] [] + ALOAD 0 + ALOAD 1 + INVOKESPECIAL com/android/volley/NetworkDispatcher.addTrafficStatsTag (Lcom/android/volley/Request;)V + L18 + LINENUMBER 132 L18 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mNetwork : Lcom/android/volley/Network; + ALOAD 1 + INVOKEINTERFACE com/android/volley/Network.performRequest (Lcom/android/volley/Request;)Lcom/android/volley/NetworkResponse; (itf) + ASTORE 4 + L19 + LINENUMBER 133 L19 + ALOAD 1 + LDC "network-http-complete" + INVOKEVIRTUAL com/android/volley/Request.addMarker (Ljava/lang/String;)V + L20 + LINENUMBER 137 L20 + ALOAD 4 + GETFIELD com/android/volley/NetworkResponse.notModified : Z + IFEQ L5 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.hasHadResponseDelivered ()Z + IFEQ L5 + L21 + LINENUMBER 138 L21 + ALOAD 1 + LDC "not-modified" + INVOKEVIRTUAL com/android/volley/Request.finish (Ljava/lang/String;)V + L22 + LINENUMBER 139 L22 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.notifyListenerResponseNotUsable ()V + L4 + LINENUMBER 169 L4 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L23 + LINENUMBER 140 L23 + RETURN + L5 + LINENUMBER 144 L5 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J com/android/volley/NetworkResponse] [] + ALOAD 1 + ALOAD 4 + INVOKEVIRTUAL com/android/volley/Request.parseNetworkResponse (Lcom/android/volley/NetworkResponse;)Lcom/android/volley/Response; + ASTORE 5 + L24 + LINENUMBER 145 L24 + ALOAD 1 + LDC "network-parse-complete" + INVOKEVIRTUAL com/android/volley/Request.addMarker (Ljava/lang/String;)V + L25 + LINENUMBER 149 L25 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.shouldCache ()Z + IFEQ L26 + ALOAD 5 + GETFIELD com/android/volley/Response.cacheEntry : Lcom/android/volley/Cache$Entry; + IFNULL L26 + L27 + LINENUMBER 150 L27 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mCache : Lcom/android/volley/Cache; + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.getCacheKey ()Ljava/lang/String; + ALOAD 5 + GETFIELD com/android/volley/Response.cacheEntry : Lcom/android/volley/Cache$Entry; + INVOKEINTERFACE com/android/volley/Cache.put (Ljava/lang/String;Lcom/android/volley/Cache$Entry;)V (itf) + L28 + LINENUMBER 151 L28 + ALOAD 1 + LDC "network-cache-written" + INVOKEVIRTUAL com/android/volley/Request.addMarker (Ljava/lang/String;)V + L26 + LINENUMBER 155 L26 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J com/android/volley/NetworkResponse com/android/volley/Response] [] + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.markDelivered ()V + L29 + LINENUMBER 156 L29 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mDelivery : Lcom/android/volley/ResponseDelivery; + ALOAD 1 + ALOAD 5 + INVOKEINTERFACE com/android/volley/ResponseDelivery.postResponse (Lcom/android/volley/Request;Lcom/android/volley/Response;)V (itf) + L30 + LINENUMBER 157 L30 + ALOAD 1 + ALOAD 5 + INVOKEVIRTUAL com/android/volley/Request.notifyListenerResponseReceived (Lcom/android/volley/Response;)V + L6 + LINENUMBER 169 L6 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L31 + LINENUMBER 170 L31 + GOTO L32 + L2 + LINENUMBER 158 L2 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J] [com/android/volley/VolleyError] + ASTORE 4 + L33 + LINENUMBER 159 L33 + ALOAD 4 + INVOKESTATIC android/os/SystemClock.elapsedRealtime ()J + LLOAD 2 + LSUB + INVOKEVIRTUAL com/android/volley/VolleyError.setNetworkTimeMs (J)V + L34 + LINENUMBER 160 L34 + ALOAD 0 + ALOAD 1 + ALOAD 4 + INVOKESPECIAL com/android/volley/NetworkDispatcher.parseAndDeliverNetworkError (Lcom/android/volley/Request;Lcom/android/volley/VolleyError;)V + L35 + LINENUMBER 161 L35 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.notifyListenerResponseNotUsable ()V + L9 + LINENUMBER 169 L9 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L36 + LINENUMBER 170 L36 + GOTO L32 + L7 + LINENUMBER 162 L7 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J] [java/lang/Exception] + ASTORE 4 + L37 + LINENUMBER 163 L37 + ALOAD 4 + LDC "Unhandled exception %s" + ICONST_1 + ANEWARRAY java/lang/Object + DUP + ICONST_0 + ALOAD 4 + INVOKEVIRTUAL java/lang/Exception.toString ()Ljava/lang/String; + AASTORE + INVOKESTATIC com/android/volley/VolleyLog.e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V + L38 + LINENUMBER 164 L38 + NEW com/android/volley/VolleyError + DUP + ALOAD 4 + INVOKESPECIAL com/android/volley/VolleyError. (Ljava/lang/Throwable;)V + ASTORE 5 + L39 + LINENUMBER 165 L39 + ALOAD 5 + INVOKESTATIC android/os/SystemClock.elapsedRealtime ()J + LLOAD 2 + LSUB + INVOKEVIRTUAL com/android/volley/VolleyError.setNetworkTimeMs (J)V + L40 + LINENUMBER 166 L40 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mDelivery : Lcom/android/volley/ResponseDelivery; + ALOAD 1 + ALOAD 5 + INVOKEINTERFACE com/android/volley/ResponseDelivery.postError (Lcom/android/volley/Request;Lcom/android/volley/VolleyError;)V (itf) + L41 + LINENUMBER 167 L41 + ALOAD 1 + INVOKEVIRTUAL com/android/volley/Request.notifyListenerResponseNotUsable ()V + L10 + LINENUMBER 169 L10 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + L42 + LINENUMBER 170 L42 + GOTO L32 + L8 + LINENUMBER 169 L8 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J] [java/lang/Throwable] + ASTORE 6 + L11 + ALOAD 1 + ICONST_4 + INVOKEVIRTUAL com/android/volley/Request.sendEvent (I)V + ALOAD 6 + ATHROW + L32 + LINENUMBER 171 L32 + FRAME FULL [com/android/volley/NetworkDispatcher com/android/volley/Request J] [] + RETURN + L43 + LOCALVARIABLE networkResponse Lcom/android/volley/NetworkResponse; L19 L6 4 + LOCALVARIABLE response Lcom/android/volley/Response; L24 L6 5 + // signature Lcom/android/volley/Response<*>; + // declaration: response extends com.android.volley.Response + LOCALVARIABLE volleyError Lcom/android/volley/VolleyError; L33 L9 4 + LOCALVARIABLE volleyError Lcom/android/volley/VolleyError; L39 L10 5 + LOCALVARIABLE e Ljava/lang/Exception; L37 L10 4 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L12 L43 0 + LOCALVARIABLE request Lcom/android/volley/Request; L12 L43 1 + // signature Lcom/android/volley/Request<*>; + // declaration: request extends com.android.volley.Request + LOCALVARIABLE startTimeMs J L13 L43 2 + MAXSTACK = 6 + MAXLOCALS = 7 + + // access flags 0x2 + // signature (Lcom/android/volley/Request<*>;Lcom/android/volley/VolleyError;)V + // declaration: void parseAndDeliverNetworkError(com.android.volley.Request, com.android.volley.VolleyError) + private parseAndDeliverNetworkError(Lcom/android/volley/Request;Lcom/android/volley/VolleyError;)V + ALOAD 1 + ALOAD 2 + INVOKESTATIC io/embrace/android/embracesdk/volley/callback/com/android/volley/NetworkDispatcher._preParseAndDeliverNetworkError (Lcom/android/volley/Request;Lcom/android/volley/VolleyError;)V + L0 + LINENUMBER 174 L0 + ALOAD 1 + ALOAD 2 + INVOKEVIRTUAL com/android/volley/Request.parseNetworkError (Lcom/android/volley/VolleyError;)Lcom/android/volley/VolleyError; + ASTORE 2 + L1 + LINENUMBER 175 L1 + ALOAD 0 + GETFIELD com/android/volley/NetworkDispatcher.mDelivery : Lcom/android/volley/ResponseDelivery; + ALOAD 1 + ALOAD 2 + INVOKEINTERFACE com/android/volley/ResponseDelivery.postError (Lcom/android/volley/Request;Lcom/android/volley/VolleyError;)V (itf) + L2 + LINENUMBER 176 L2 + RETURN + L3 + LOCALVARIABLE this Lcom/android/volley/NetworkDispatcher; L0 L3 0 + LOCALVARIABLE request Lcom/android/volley/Request; L0 L3 1 + // signature Lcom/android/volley/Request<*>; + // declaration: request extends com.android.volley.Request + LOCALVARIABLE error Lcom/android/volley/VolleyError; L0 L3 2 + MAXSTACK = 3 + MAXLOCALS = 3 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/NoOverrideWebViewClient_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/NoOverrideWebViewClient_expected.txt new file mode 100644 index 0000000000..87f0838f74 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/NoOverrideWebViewClient_expected.txt @@ -0,0 +1,35 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/NoOverrideWebViewClient extends android/webkit/WebViewClient { + + // compiled from: NoOverrideWebViewClient.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002\u00a8\u0006\u0003"}, d2={"Lio/embrace/test/fixtures/NoOverrideWebViewClient;", "Landroid/webkit/WebViewClient;", "()V", "embrace-bytecode-instrumentation-tests_release"}) + + // access flags 0x1 + public ()V + L0 + LINENUMBER 8 L0 + ALOAD 0 + INVOKESPECIAL android/webkit/WebViewClient. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/NoOverrideWebViewClient; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onPageStarted(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESTATIC io/embrace/android/embracesdk/WebViewClientSwazzledHooks._preOnPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + ALOAD 0 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESPECIAL android/webkit/WebViewClient.onPageStarted (Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V + RETURN + MAXSTACK = 4 + MAXLOCALS = 0 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickInnerListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickInnerListener_expected.txt new file mode 100644 index 0000000000..255d27e6b1 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickInnerListener_expected.txt @@ -0,0 +1,50 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener implements android/view/View$OnLongClickListener { + + // compiled from: KotlinNestedOnLongClick.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0018\u0002\n\u0000\u0008\u0086\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener;", "Landroid/view/View$OnLongClickListener;", "(Lio/embrace/test/fixtures/KotlinNestedOnLongClick;)V", "onLongClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + // access flags 0x11 + public final INNERCLASS io/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener io/embrace/test/fixtures/KotlinNestedOnLongClick OnLongClickInnerListener + + // access flags 0x1010 + final synthetic Lio/embrace/test/fixtures/KotlinNestedOnLongClick; this$0 + + // access flags 0x1 + // signature ()V + // declaration: void () + public (Lio/embrace/test/fixtures/KotlinNestedOnLongClick;)V + L0 + LINENUMBER 9 L0 + ALOAD 0 + ALOAD 1 + PUTFIELD io/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener.this$0 : Lio/embrace/test/fixtures/KotlinNestedOnLongClick; + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener; L0 L1 0 + LOCALVARIABLE this$0 Lio/embrace/test/fixtures/KotlinNestedOnLongClick; L0 L1 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 11 L0 + ICONST_1 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickInnerListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickStaticListener_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickStaticListener_expected.txt new file mode 100644 index 0000000000..56b4834c1b --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/OnLongClickStaticListener_expected.txt @@ -0,0 +1,41 @@ +// class version 55.0 (55) +// access flags 0x31 +public final class io/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickStaticListener implements android/view/View$OnLongClickListener { + + // compiled from: KotlinNestedOnLongClick.kt + + @Lkotlin/Metadata;(mv={1, 8, 0}, k=1, xi=48, d1={"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\u0008\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0016\u00a8\u0006\u0007"}, d2={"Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickStaticListener;", "Landroid/view/View$OnLongClickListener;", "()V", "onLongClick", "", "view", "Landroid/view/View;", "embrace-bytecode-instrumentation-tests_release"}) + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnLongClickListener android/view/View OnLongClickListener + // access flags 0x19 + public final static INNERCLASS io/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickStaticListener io/embrace/test/fixtures/KotlinNestedOnLongClick OnLongClickStaticListener + + // access flags 0x1 + public ()V + L0 + LINENUMBER 15 L0 + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickStaticListener; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x1 + public onLongClick(Landroid/view/View;)Z + // annotable parameter count: 1 (invisible) + @Lorg/jetbrains/annotations/Nullable;() // invisible, parameter 0 + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnLongClickListener._preOnLongClick (Landroid/view/View$OnLongClickListener;Landroid/view/View;)V + L0 + LINENUMBER 17 L0 + ICONST_1 + IRETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/KotlinNestedOnLongClick$OnLongClickStaticListener; L0 L1 0 + LOCALVARIABLE view Landroid/view/View; L0 L1 1 + MAXSTACK = 1 + MAXLOCALS = 2 +} diff --git a/embrace-bytecode-instrumentation-tests/src/test/resources/VirtualMethodRefNamedOnClick_expected.txt b/embrace-bytecode-instrumentation-tests/src/test/resources/VirtualMethodRefNamedOnClick_expected.txt new file mode 100644 index 0000000000..d476161785 --- /dev/null +++ b/embrace-bytecode-instrumentation-tests/src/test/resources/VirtualMethodRefNamedOnClick_expected.txt @@ -0,0 +1,83 @@ +// class version 55.0 (55) +// access flags 0x21 +public class io/embrace/test/fixtures/VirtualMethodRefNamedOnClick extends androidx/fragment/app/Fragment { + + // compiled from: VirtualMethodRefNamedOnClick.java + // access flags 0x609 + public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener + // access flags 0x19 + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + + // access flags 0x1 + public ()V + L0 + LINENUMBER 18 L0 + ALOAD 0 + INVOKESPECIAL androidx/fragment/app/Fragment. ()V + RETURN + L1 + LOCALVARIABLE this Lio/embrace/test/fixtures/VirtualMethodRefNamedOnClick; L0 L1 0 + MAXSTACK = 1 + MAXLOCALS = 1 + + // access flags 0x2 + private onClick(Landroid/view/View;)V + ALOAD 0 + ALOAD 1 + INVOKESTATIC io/embrace/android/embracesdk/ViewSwazzledHooks$OnClickListener._preOnClick (Landroid/view/View$OnClickListener;Landroid/view/View;)V + L0 + LINENUMBER 21 L0 + LDC "Embrace" + LDC "test" + INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I + POP + L1 + LINENUMBER 22 L1 + RETURN + L2 + LOCALVARIABLE this Lio/embrace/test/fixtures/VirtualMethodRefNamedOnClick; L0 L2 0 + LOCALVARIABLE lambdaView Landroid/view/View; L0 L2 1 + MAXSTACK = 2 + MAXLOCALS = 2 + + // access flags 0x1 + public onCreateView(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + // annotable parameter count: 3 (invisible) + @Landroidx/annotation/NonNull;() // invisible, parameter 0 + L0 + LINENUMBER 26 L0 + ALOAD 0 + ALOAD 1 + ALOAD 2 + ALOAD 3 + INVOKESPECIAL androidx/fragment/app/Fragment.onCreateView (Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)Landroid/view/View; + ASTORE 4 + L1 + LINENUMBER 27 L1 + ALOAD 4 + INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object; + CHECKCAST android/view/View + ALOAD 0 + INVOKEDYNAMIC onClick(Lio/embrace/test/fixtures/VirtualMethodRefNamedOnClick;)Landroid/view/View$OnClickListener; [ + // handle kind 0x6 : INVOKESTATIC + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + // arguments: + (Landroid/view/View;)V, + // handle kind 0x7 : INVOKESPECIAL + io/embrace/test/fixtures/VirtualMethodRefNamedOnClick.onClick(Landroid/view/View;)V, + (Landroid/view/View;)V + ] + INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V + L2 + LINENUMBER 28 L2 + ALOAD 4 + ARETURN + L3 + LOCALVARIABLE this Lio/embrace/test/fixtures/VirtualMethodRefNamedOnClick; L0 L3 0 + LOCALVARIABLE inflater Landroid/view/LayoutInflater; L0 L3 1 + LOCALVARIABLE container Landroid/view/ViewGroup; L0 L3 2 + LOCALVARIABLE savedInstanceState Landroid/os/Bundle; L0 L3 3 + LOCALVARIABLE view Landroid/view/View; L1 L3 4 + MAXSTACK = 4 + MAXLOCALS = 5 +} diff --git a/embrace-gradle-plugin-integration-tests/README.md b/embrace-gradle-plugin-integration-tests/README.md new file mode 100644 index 0000000000..632bfaa380 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/README.md @@ -0,0 +1,18 @@ +# Debugging Embrace Gradle Plugin +You can attach a debugger to an integration test by: + +1. Set `attachDebugger = true` on the test case +2. Select 'Edit Configurations' in Android Studio +3. Add & save a new 'Remote JVM Debug' configuration +4. Run the test case (without a debugger) +5. Set a breakpoint where you require in the gradle plugin code +6. Run the 'Remote JVM Debug' configuration +7. Debug as you normally would! + +## Debugging the debugger +If the debugger can't attach, please check whether the port is in use via `lsof -i :5005` +and kill the process if necessary. Running `./gradlew --stop` may also help. + +## How does it work? +Setting `attachDebugger = true` adds `-Dorg.gradle.debug=true` and `--no-daemon` to the Gradle TestKit arguments. +This principle can be used anytime a gradle task needs debugging and doesn't strictly need to happen in an integration test. diff --git a/embrace-gradle-plugin-integration-tests/build.gradle.kts b/embrace-gradle-plugin-integration-tests/build.gradle.kts new file mode 100644 index 0000000000..730e472276 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + kotlin("jvm") + alias(libs.plugins.google.ksp) + id("java-gradle-plugin") + id("maven-publish") + id("io.embrace.internal.build-logic") +} + +embrace { + productionModule.set(false) + androidModule.set(false) + jvmTarget.set(JavaVersion.VERSION_11) +} + +dependencies { + implementation(gradleApi()) + implementation(gradleTestKit()) + implementation(project(":embrace-gradle-plugin")) + implementation(libs.agp.api) + + // JSON construction and parsing + implementation(libs.moshi) + ksp(libs.moshi.kotlin.codegen) + + implementation(libs.junit) + implementation(libs.mockwebserver) + testImplementation(project(":embrace-test-common")) + implementation(libs.zstd.jni) + implementation(libs.bundletool) + implementation(libs.apktool.lib) +} + +// ensure that the plugin is published to maven local before running integration tests +tasks.withType(Test::class.java).configureEach { + dependsOn(":embrace-gradle-plugin-integration-tests:publishToMavenLocal") + dependsOn(":embrace-gradle-plugin:publishToMavenLocal") +} + +group = "io.embrace" +version = project.properties["version"] as String + +if (!gradle.startParameter.taskNames.any { it == "publishToSonatype" || it == "closeSonatypeStagingRepository" }) { + gradlePlugin { + plugins { + create("integrationTestPlugin") { + id = "io.embrace.android.testplugin" + implementationClass = + "io.embrace.android.gradle.integration.framework.IntegrationTestPlugin" + } + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/build.gradle new file mode 100644 index 0000000000..6d18dc2e53 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/build.gradle @@ -0,0 +1,8 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) +integrationTest.configure3rdPartyLibrary(project) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/build.gradle new file mode 100644 index 0000000000..f5f2d8cf73 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/build.gradle @@ -0,0 +1,31 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") version '1.9.10' +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +android { + namespace 'com.example.customLibrary' + compileSdk 34 + + defaultConfig { + minSdk 24 + targetSdk 34 + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' // Supported architectures + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" // Path to the CMake build script + version = "3.22.1" + } + } +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..454b143fbf --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.22.1) + +add_library( + emb-crisps + SHARED + crisps.c +) + +add_library( + emb-donuts + SHARED + donuts.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/settings.gradle.kts b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/settings.gradle.kts new file mode 100644 index 0000000000..5a710d4d48 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/settings.gradle.kts @@ -0,0 +1,17 @@ +include(":customLibrary") + +pluginManagement { + repositories { + google() + mavenCentral() + mavenLocal() + } + plugins { + val agpVersion = extra["agp_version"] as String + val snapshotVersion = extra["plugin_snapshot_version"] as String + + id("com.android.application").version(agpVersion) + id("io.embrace.swazzler").version(snapshotVersion) + id("io.embrace.android.testplugin").version(snapshotVersion) + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/src/main/embrace-config.json new file mode 100644 index 0000000000..bcd7496a71 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-3rd-party-symbols/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": true +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-build-types-product-flavors/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-build-types-product-flavors/build.gradle new file mode 100644 index 0000000000..6b550a743c --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-build-types-product-flavors/build.gradle @@ -0,0 +1,30 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + buildTypes { + custom { + minifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + } + } + flavorDimensions "apple" + productFlavors { + demo { + dimension "apple" + versionNameSuffix "-demo" + } + full { + dimension "apple" + versionNameSuffix "-full" + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-build-types/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-build-types/build.gradle new file mode 100644 index 0000000000..7deaaff26e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-build-types/build.gradle @@ -0,0 +1,19 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + buildTypes { + custom { + minifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/build.gradle new file mode 100644 index 0000000000..27d740a186 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..bd77c2bf15 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10.2) + +add_library( + emb-crisps + SHARED + crisps.c +) + +add_library( + emb-donuts + SHARED + donuts.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/embrace-config.json new file mode 100644 index 0000000000..f80073d3bf --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake-ndk-disabled/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": false +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/build.gradle new file mode 100644 index 0000000000..27d740a186 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..bd77c2bf15 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10.2) + +add_library( + emb-crisps + SHARED + crisps.c +) + +add_library( + emb-donuts + SHARED + donuts.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/embrace-config.json new file mode 100644 index 0000000000..bcd7496a71 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-cmake/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": true +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-disable-product-flavor/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-disable-product-flavor/build.gradle new file mode 100644 index 0000000000..cfde2cc187 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-disable-product-flavor/build.gradle @@ -0,0 +1,29 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + flavorDimensions "apple" + productFlavors { + demo { + dimension "apple" + versionNameSuffix "-demo" + } + full { + dimension "apple" + versionNameSuffix "-full" + } + } +} + +swazzler { + variantFilter { + if (it.name.contains("demoRelease")) { + it.disablePluginForVariant() + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-flavor-dimensions/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-flavor-dimensions/build.gradle new file mode 100644 index 0000000000..2ace58c585 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-flavor-dimensions/build.gradle @@ -0,0 +1,30 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + buildTypes { + custom { + minifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + } + } + flavorDimensions "apple", "banana" + productFlavors { + demo { + dimension "apple" + versionNameSuffix "-demo" + } + full { + dimension "banana" + versionNameSuffix "-full" + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/build.gradle new file mode 100644 index 0000000000..c4873b1932 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/build.gradle @@ -0,0 +1,16 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) +integrationTest.configure3rdPartyLibrary(project) + +android { + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + } + } +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/build.gradle new file mode 100644 index 0000000000..f5f2d8cf73 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/build.gradle @@ -0,0 +1,31 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") version '1.9.10' +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +android { + namespace 'com.example.customLibrary' + compileSdk 34 + + defaultConfig { + minSdk 24 + targetSdk 34 + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' // Supported architectures + } + } + + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" // Path to the CMake build script + version = "3.22.1" + } + } +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..454b143fbf --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.22.1) + +add_library( + emb-crisps + SHARED + crisps.c +) + +add_library( + emb-donuts + SHARED + donuts.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/customLibrary/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/settings.gradle.kts b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/settings.gradle.kts new file mode 100644 index 0000000000..5a710d4d48 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/settings.gradle.kts @@ -0,0 +1,17 @@ +include(":customLibrary") + +pluginManagement { + repositories { + google() + mavenCentral() + mavenLocal() + } + plugins { + val agpVersion = extra["agp_version"] as String + val snapshotVersion = extra["plugin_snapshot_version"] as String + + id("com.android.application").version(agpVersion) + id("io.embrace.swazzler").version(snapshotVersion) + id("io.embrace.android.testplugin").version(snapshotVersion) + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..2d5780531d --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.10.2) + +add_library( + emb-asado + SHARED + asado.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/asado.c b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/asado.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/cpp/asado.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/embrace-config.json new file mode 100644 index 0000000000..bcd7496a71 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-local-and-3rd-party-symbols/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": true +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/build.gradle new file mode 100644 index 0000000000..ea1f2ba316 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/build.gradle @@ -0,0 +1,16 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + ndkVersion "27.0.12077973" + externalNativeBuild { + ndkBuild { + path "src/main/cpp/Android.mk" + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/Android.mk b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/Android.mk new file mode 100644 index 0000000000..a381d7d368 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/Android.mk @@ -0,0 +1,13 @@ +LOCAL_PATH := $(call my-dir) + +# Configuration for emb-crisps library +include $(CLEAR_VARS) +LOCAL_MODULE := emb-crisps +LOCAL_SRC_FILES := crisps.c +include $(BUILD_SHARED_LIBRARY) + +# Configuration for emb-donuts library +include $(CLEAR_VARS) +LOCAL_MODULE := emb-donuts +LOCAL_SRC_FILES := donuts.c +include $(BUILD_SHARED_LIBRARY) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/embrace-config.json new file mode 100644 index 0000000000..bcd7496a71 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-ndk-build/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": true +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/build.gradle new file mode 100644 index 0000000000..4cbcf1991e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/build.gradle @@ -0,0 +1,7 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/AndroidManifest.xml b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..50e0a262fc --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/embrace-config.json new file mode 100644 index 0000000000..7cda90d4de --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/app/src/main/embrace-config.json @@ -0,0 +1,4 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512" +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-nested/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/build.gradle new file mode 100644 index 0000000000..e69de29bb2 diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-nested/settings.gradle.kts b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/settings.gradle.kts new file mode 100644 index 0000000000..93fee72591 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-nested/settings.gradle.kts @@ -0,0 +1,17 @@ +include(":app") + +pluginManagement { + repositories { + google() + mavenCentral() + mavenLocal() + } + plugins { + val agpVersion = extra["agp_version"] as String + val snapshotVersion = extra["plugin_snapshot_version"] as String + + id("com.android.application").version(agpVersion) + id("io.embrace.swazzler").version(snapshotVersion) + id("io.embrace.android.testplugin").version(snapshotVersion) + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-product-flavors/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-product-flavors/build.gradle new file mode 100644 index 0000000000..6b8efdfb81 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-product-flavors/build.gradle @@ -0,0 +1,21 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + flavorDimensions "apple" + productFlavors { + demo { + dimension "apple" + versionNameSuffix "-demo" + } + full { + dimension "apple" + versionNameSuffix "-full" + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-simple/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/android-simple/build.gradle new file mode 100644 index 0000000000..4cbcf1991e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-simple/build.gradle @@ -0,0 +1,7 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/android-version-support/build.gradle.kts b/embrace-gradle-plugin-integration-tests/fixtures/android-version-support/build.gradle.kts new file mode 100644 index 0000000000..de8330cffa --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/android-version-support/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") + id("org.jetbrains.kotlin.android") +} + +integrationTest.configureAndroidProject(project) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-compression-missing-input/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-missing-input/build.gradle new file mode 100644 index 0000000000..8c1b45398b --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-missing-input/build.gradle @@ -0,0 +1,17 @@ +import io.embrace.android.gradle.plugin.tasks.common.FileCompressionTask + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", FileCompressionTask) { task -> + task.originalFile.set( + project.layout.projectDirectory.file("mapping.txt") + ) + task.compressedFile.set( + project.layout.buildDirectory.file("mapping.txt") + ) + integrationTest.configureEmbraceTask(task) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/build.gradle new file mode 100644 index 0000000000..897eddb52c --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/build.gradle @@ -0,0 +1,17 @@ +import io.embrace.android.gradle.plugin.tasks.common.FileCompressionTask + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", FileCompressionTask) { task -> + task.originalFile.set( + project.layout.projectDirectory.file("input.txt") + ) + task.compressedFile.set( + project.layout.buildDirectory.file("mapping.txt") + ) + integrationTest.configureEmbraceTask(task) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/input.txt b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/input.txt new file mode 100644 index 0000000000..5dd01c177f --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-compression-simple/input.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-upload-missing-input/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-missing-input/build.gradle new file mode 100644 index 0000000000..742fc6ed31 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-missing-input/build.gradle @@ -0,0 +1,16 @@ +import io.embrace.android.gradle.plugin.tasks.common.MultipartUploadTask +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", MultipartUploadTask) { task -> + integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.PROGUARD, "my-filename.txt") + + task.uploadFile.set( + project.layout.projectDirectory.file("input.txt") + ) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/build.gradle new file mode 100644 index 0000000000..d34de50b39 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/build.gradle @@ -0,0 +1,16 @@ +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.common.MultipartUploadTask + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", MultipartUploadTask) { task -> + integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.NDK, "my-filename.txt") + + task.uploadFile.set( + project.layout.projectDirectory.file("input.txt") + ) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/input.txt b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/input.txt new file mode 100644 index 0000000000..5dd01c177f --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-set-endpoint/input.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/build.gradle new file mode 100644 index 0000000000..742fc6ed31 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/build.gradle @@ -0,0 +1,16 @@ +import io.embrace.android.gradle.plugin.tasks.common.MultipartUploadTask +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", MultipartUploadTask) { task -> + integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.PROGUARD, "my-filename.txt") + + task.uploadFile.set( + project.layout.projectDirectory.file("input.txt") + ) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/input.txt b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/input.txt new file mode 100644 index 0000000000..5dd01c177f --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/file-upload-simple/input.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/build.gradle new file mode 100644 index 0000000000..7809e80ac1 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/build.gradle @@ -0,0 +1,25 @@ +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadTask +import io.embrace.android.gradle.plugin.tasks.ndk.NdkType +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", NdkUploadTask) { task -> + integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.NDK, null) + task.ndkType.set(NdkType.NATIVE) + task.ndkEnabled.set(true) + + task.architecturesDirectoryForNative.from( + project.layout.projectDirectory.dir("native-libs") + ) + task.generatedEmbraceResourcesDirectory.set( + project.layout.buildDirectory.dir("generated-embrace-resources") + ) + task.deobfuscatedFilesDirPath.set( + project.layout.buildDirectory.dir("deobfuscated-files") + ) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/handshake.json b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/handshake.json new file mode 100644 index 0000000000..1c734e4700 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/handshake.json @@ -0,0 +1,6 @@ +{ + "app": "abcde", + "token": "12345123451234512345123451234512", + "variant": "demoDevelopmentRelease", + "archs": {} +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/ndk_symbols.xml b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/ndk_symbols.xml new file mode 100644 index 0000000000..98b3c22761 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/expected/ndk_symbols.xml @@ -0,0 +1,5 @@ + + + + eyJzeW1ib2xzIjp7fX0= + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/input.txt b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/input.txt new file mode 100644 index 0000000000..5dd01c177f --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/input.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/native-libs/fake-lib.so b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/native-libs/fake-lib.so new file mode 100644 index 0000000000..65f6c8ccaf --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/ndk-upload-simple/native-libs/fake-lib.so @@ -0,0 +1 @@ +Fake native library \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/App.tsx b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/App.tsx new file mode 100644 index 0000000000..125fe1b98e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/App.tsx @@ -0,0 +1,118 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + */ + +import React from 'react'; +import type {PropsWithChildren} from 'react'; +import { + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + useColorScheme, + View, +} from 'react-native'; + +import { + Colors, + DebugInstructions, + Header, + LearnMoreLinks, + ReloadInstructions, +} from 'react-native/Libraries/NewAppScreen'; + +type SectionProps = PropsWithChildren<{ + title: string; +}>; + +function Section({children, title}: SectionProps): React.JSX.Element { + const isDarkMode = useColorScheme() === 'dark'; + return ( + + + {title} + + + {children} + + + ); +} + +function App(): React.JSX.Element { + const isDarkMode = useColorScheme() === 'dark'; + + const backgroundStyle = { + backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, + }; + + return ( + + + +
+ +
+ Edit App.tsx to change this + screen and then come back to see your edits. +
+
+ +
+
+ +
+
+ Read the docs to discover what to do next: +
+ +
+ + + ); +} + +const styles = StyleSheet.create({ + sectionContainer: { + marginTop: 32, + paddingHorizontal: 24, + }, + sectionTitle: { + fontSize: 24, + fontWeight: '600', + }, + sectionDescription: { + marginTop: 8, + fontSize: 18, + fontWeight: '400', + }, + highlight: { + fontWeight: '700', + }, +}); + +export default App; diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/build.gradle new file mode 100644 index 0000000000..e2c0d8c427 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/build.gradle @@ -0,0 +1,131 @@ +apply plugin: "com.android.application" +apply plugin: 'embrace-swazzler' +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" +apply plugin: "io.embrace.android.testplugin" + +repositories { + google() + mavenCentral() + mavenLocal() +} + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js + // cliFile = file("../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc:+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "com.rnapp" + defaultConfig { + applicationId "com.rnapp" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +swazzler { + disableDependencyInjection = true +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/debug.keystore b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/debug.keystore new file mode 100644 index 0000000000..364e105ed3 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/debug.keystore differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/proguard-rules.pro b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/proguard-rules.pro new file mode 100644 index 0000000000..11b025724a --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/proguard-rules.pro @@ -0,0 +1,10 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/AndroidManifest.xml b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e1892528b8 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/embrace-config.json new file mode 100644 index 0000000000..e2d4542445 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/embrace-config.json @@ -0,0 +1,4 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512" +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainActivity.kt b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainActivity.kt new file mode 100644 index 0000000000..4469be42fb --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainActivity.kt @@ -0,0 +1,22 @@ +package com.rnapp + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "rnapp" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate = + DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainApplication.kt b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainApplication.kt new file mode 100644 index 0000000000..d238c9c45e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/java/com/rnapp/MainApplication.kt @@ -0,0 +1,44 @@ +package com.rnapp + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(this) { + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + } + + override fun getJSMainModuleName(): String = "index" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + + override val reactHost: ReactHost + get() = getDefaultReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/drawable/rn_edit_text_material.xml b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000000..5c25e728ea --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..a2f5908281 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..1b52399808 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..ff10afd6e1 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..115a4c768a Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..dcd3cd8083 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..459ca609d3 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..8ca12fe024 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..8e19b410a1 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..b824ebdd48 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..4c19a13c23 Binary files /dev/null and b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/strings.xml b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..d46c209120 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + rnapp + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/styles.xml b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..7ba83a2ad5 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/build.gradle new file mode 100644 index 0000000000..973fff838d --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/build.gradle @@ -0,0 +1,24 @@ +buildscript { + ext { + buildToolsVersion = "35.0.0" + minSdkVersion = 24 + compileSdkVersion = 35 + targetSdkVersion = 34 + ndkVersion = "26.1.10909125" + kotlinVersion = "1.9.24" + } + repositories { + google() + mavenCentral() + mavenLocal() + } + dependencies { + classpath("com.android.tools.build:gradle:$agp_version") + classpath "io.embrace:embrace-swazzler:$plugin_snapshot_version" + classpath "io.embrace:embrace-gradle-plugin-integration-tests:$plugin_snapshot_version" + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/gradle.properties b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/gradle.properties new file mode 100644 index 0000000000..795db018fb --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/gradle.properties @@ -0,0 +1,25 @@ +# Project-wide Gradle settings. +org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g +org.gradle.parallel=true +org.gradle.caching=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/settings.gradle b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/settings.gradle new file mode 100644 index 0000000000..c14849005b --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'rnapp' +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/app.json b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/app.json new file mode 100644 index 0000000000..f389f574f4 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/app.json @@ -0,0 +1,4 @@ +{ + "name": "rnapp", + "displayName": "rnapp" +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/index.js b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/index.js new file mode 100644 index 0000000000..a850d031de --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/index.js @@ -0,0 +1,9 @@ +/** + * @format + */ + +import {AppRegistry} from 'react-native'; +import App from './App'; +import {name as appName} from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/metro.config.js b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/metro.config.js new file mode 100644 index 0000000000..9d41685ef1 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/metro.config.js @@ -0,0 +1,11 @@ +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('metro-config').MetroConfig} + */ +const config = {}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/package.json b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/package.json new file mode 100644 index 0000000000..cb30b73aff --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/package.json @@ -0,0 +1,26 @@ +{ + "name": "rnapp", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android", + "lint": "eslint .", + "start": "react-native start" + }, + "dependencies": { + "@embrace-io/react-native": "^5.1.0", + "react": "18.3.1", + "react-native": "0.76.5" + }, + "devDependencies": { + "@react-native-community/cli": "15.0.1", + "@react-native-community/cli-platform-android": "15.0.1", + "@react-native/metro-config": "0.76.5", + "@react-native/typescript-config": "0.76.5", + "@types/react": "^18.2.6", + "typescript": "5.0.4" + }, + "engines": { + "node": ">=18" + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/tsconfig.json b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/tsconfig.json new file mode 100644 index 0000000000..4b041bce6a --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/react-native-android/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@react-native/typescript-config/tsconfig.json", + "compilerOptions": { + "jsx": "react" + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/build.gradle new file mode 100644 index 0000000000..a92fb55c03 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/build.gradle @@ -0,0 +1,25 @@ +import io.embrace.android.gradle.plugin.tasks.reactnative.EmbraceRnSourcemapGeneratorTask +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint + +plugins { + id("java") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +project.tasks.register("testTask", EmbraceRnSourcemapGeneratorTask) { task -> + integrationTest.configureGradleUploadTask(project, task, EmbraceEndpoint.SOURCE_MAP, "sourcemap.json") + + task.generatedEmbraceResourcesDirectory.set( + project.layout.buildDirectory.dir("generated-embrace-resources") + ) + task.sourcemap.set( + project.layout.projectDirectory.file("source.map") + ) + task.bundleFile.set( + project.layout.projectDirectory.file("bundle.js") + ) + task.sourcemapAndBundleFile.set( + project.layout.buildDirectory.file("output") + ) +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/bundle.js b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/bundle.js new file mode 100644 index 0000000000..aade829a09 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/bundle.js @@ -0,0 +1 @@ +Fake bundle \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/expected/rn_sourcemap.xml b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/expected/rn_sourcemap.xml new file mode 100644 index 0000000000..181b46bbc3 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/expected/rn_sourcemap.xml @@ -0,0 +1,5 @@ + + + + B3338A366B74EB89C112C8BB13C1B570 + diff --git a/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/source.map b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/source.map new file mode 100644 index 0000000000..3a627513c2 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/rn-upload-simple/source.map @@ -0,0 +1 @@ +Fake sourcemap \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/build.gradle b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/build.gradle new file mode 100644 index 0000000000..27d740a186 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("com.android.application") + id("io.embrace.swazzler") + id("io.embrace.android.testplugin") +} + +integrationTest.configureAndroidProject(project) + +android { + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/gradle.properties b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/gradle.properties new file mode 100644 index 0000000000..a59deaadab --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/gradle.properties @@ -0,0 +1 @@ +embrace.uploadIl2CppMappingFiles=true diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/CMakeLists.txt b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..bd77c2bf15 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.10.2) + +add_library( + emb-crisps + SHARED + crisps.c +) + +add_library( + emb-donuts + SHARED + donuts.c +) diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/crisps.c b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/crisps.c new file mode 100644 index 0000000000..fab50f2671 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/crisps.c @@ -0,0 +1,5 @@ +#include + +int foo() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/donuts.c b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/donuts.c new file mode 100644 index 0000000000..4ca2a29fb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/cpp/donuts.c @@ -0,0 +1,5 @@ +#include + +int bar() { + return 2; +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/embrace-config.json b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/embrace-config.json new file mode 100644 index 0000000000..bcd7496a71 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/src/main/embrace-config.json @@ -0,0 +1,5 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512", + "ndk_enabled": true +} diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/LineNumberMappings.json b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/LineNumberMappings.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/LineNumberMappings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/MethodMap.tsv b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/MethodMap.tsv new file mode 100644 index 0000000000..96c906756d --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/fixtures/unity-fake-project/unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols/MethodMap.tsv @@ -0,0 +1 @@ +foo bar \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/settings.gradle.kts b/embrace-gradle-plugin-integration-tests/settings.gradle.kts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/embrace-gradle-plugin-integration-tests/src/main/AndroidManifest.xml b/embrace-gradle-plugin-integration-tests/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..fd786ebfff --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/BundleToolApkBuilder.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/BundleToolApkBuilder.kt new file mode 100644 index 0000000000..6cae1ca3e2 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/BundleToolApkBuilder.kt @@ -0,0 +1,63 @@ +package io.embrace.android.gradle.integration.framework + +import com.android.tools.build.bundletool.androidtools.Aapt2Command +import com.android.tools.build.bundletool.commands.BuildApksCommand +import java.io.File +import java.util.zip.ZipFile + +class BundleToolApkBuilder { + + private val aapt2File = File("${System.getenv("ANDROID_HOME")}/build-tools/35.0.0/aapt2") + + /** + * Generates a universal APK from the given bundle file. + * Returns the generated APK file, or null if generation fails. + */ + fun generateApkFromBundle(bundleFile: File): File { + if (!aapt2File.exists()) { + error( + "Please set the ANDROID_BUILD_TOOLS_HOME environment variable. This is needed in order to use bundletool." + ) + } + if (!bundleFile.exists()) { + error("Bundle file does not exist at ${bundleFile.path}") + } + + val outputFile = File.createTempFile("generated-apks", ".apks") + + BuildApksCommand + .builder() + .setBundlePath(bundleFile.toPath()) + .setOverwriteOutput(true) + .setAapt2Command(Aapt2Command.createFromExecutablePath(aapt2File.toPath())) + .setOutputFile(outputFile.toPath()) + .setApkBuildMode(BuildApksCommand.ApkBuildMode.UNIVERSAL) + .build() + .execute() + + return extractApkFromApks(outputFile).also { + outputFile.delete() + } + } + + /** + * Extracts a universal APK from the given .apks file. + * Returns the extracted APK file, or null if extraction fails. + */ + private fun extractApkFromApks(apksFile: File): File { + val outputFile = File.createTempFile("generated-apk", ".apk") + ZipFile(apksFile).use { zip -> + zip.entries().asSequence() + .find { it.name == "universal.apk" } + ?.let { entry -> + zip.getInputStream(entry).use { input -> + outputFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + + return outputFile + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FakeApiServer.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FakeApiServer.kt new file mode 100644 index 0000000000..f17017b103 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FakeApiServer.kt @@ -0,0 +1,37 @@ +package io.embrace.android.gradle.integration.framework + +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest +import java.util.LinkedList + +class FakeApiServer : Dispatcher() { + + private val endpoints = EmbraceEndpoint.values().associateBy(EmbraceEndpoint::url) + private val receivedRequests = EmbraceEndpoint.values().associateWith { + mutableListOf() + } + private val enqueuedResponses = EmbraceEndpoint.values().associateWith { + LinkedList() + } + + override fun dispatch(request: RecordedRequest): MockResponse { + val path = request.path?.substringAfter("/api") + require(request.method == "POST") + val endpoint = endpoints[path] ?: error("Unexpected request path: ${request.path}") + checkNotNull(receivedRequests[endpoint]).add(request) + + // poll for any specific responses, otherwise just return a default 200 + val responses = checkNotNull(enqueuedResponses[endpoint]) + return responses.poll() ?: MockResponse().setResponseCode(200) + } + + fun fetchRequests(endpoint: EmbraceEndpoint): List { + return receivedRequests[endpoint]?.toList() ?: emptyList() + } + + fun enqueueResponse(endpoint: EmbraceEndpoint, response: MockResponse) { + enqueuedResponses[endpoint]?.add(response) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FileExt.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FileExt.kt new file mode 100644 index 0000000000..c9c97d9d36 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/FileExt.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.integration.framework + +import java.io.File + +/** + * Syntactic sugar that constructs a File from the project's build directory. + */ +fun File.buildFile(path: String) = File(this, "build/$path") + +/** + * Syntactic sugar that constructs a File from the project's root directory. + */ +fun File.file(path: String) = File(this, path) diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestDefaults.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestDefaults.kt new file mode 100644 index 0000000000..333786785c --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestDefaults.kt @@ -0,0 +1,7 @@ +package io.embrace.android.gradle.integration.framework + +object IntegrationTestDefaults { + const val BUILD_ID = "abcdeabcdeabcdeabcdeabcdeabcdeab" + const val APP_ID = "abcde" + const val API_TOKEN = "12345123451234512345123451234512" +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestExtension.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestExtension.kt new file mode 100644 index 0000000000..f0e6df0289 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestExtension.kt @@ -0,0 +1,95 @@ +package io.embrace.android.gradle.integration.framework + +import com.android.build.api.dsl.ApplicationExtension +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.EmbraceTask +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTask +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +/** + * An extension that can be used to configure the embrace gradle plugin in the integration tests. + * This effectively just provides default values & logic that makes it much easier to define + * fixtures. + */ +@Suppress("UnstableApiUsage") +abstract class IntegrationTestExtension(objectFactory: ObjectFactory) { + + val variantData: Property = + objectFactory.property(AndroidCompactedVariantData::class.java) + + val buildId: Property = + objectFactory.property(String::class.java).convention(IntegrationTestDefaults.BUILD_ID) + + val appId: Property = + objectFactory.property(String::class.java).convention(IntegrationTestDefaults.APP_ID) + + val apiToken: Property = + objectFactory.property(String::class.java).convention(IntegrationTestDefaults.API_TOKEN) + + fun configureEmbraceTask(task: EmbraceTask) { + task.variantData.set(variantData) + } + + fun configureGradleUploadTask( + project: Project, + task: EmbraceUploadTask, + endpoint: EmbraceEndpoint = EmbraceEndpoint.PROGUARD, + filename: String? = null + ) { + configureEmbraceTask(task) + task.requestParams.set( + RequestParams( + appId = appId.get(), + apiToken = apiToken.get(), + buildId = buildId.orNull, + endpoint = endpoint, + fileName = filename, + baseUrl = checkNotNull(project.findProperty("embrace.baseUrl")?.toString()) + ) + ) + } + + fun configureAndroidProject(project: Project) = with(project) { + repositories.apply { + google() + mavenCentral() + } + + // disable dependency injection as SNAPSHOT versions of SDK don't necessarily exist + // whenever unit tests are run + val embrace = checkNotNull(project.extensions.findByType(SwazzlerExtension::class.java)) + embrace.disableDependencyInjection.set(true) + + val android = checkNotNull(project.extensions.findByType(ApplicationExtension::class.java)) + + android.apply { + namespace = "com.example" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.app" + minSdk = 24 + versionCode = 1 + versionName = "1.0" + } + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + } + } + } + } + + fun configure3rdPartyLibrary(project: Project) = with(project) { + dependencies.add("implementation", project(":customLibrary")) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestPlugin.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestPlugin.kt new file mode 100644 index 0000000000..eb23515a8a --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/IntegrationTestPlugin.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.integration.framework + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension + +/** + * A plugin that is applied in the integration tests to make it easier to setup fixtures + * appropriately. + */ +class IntegrationTestPlugin : Plugin { + + private lateinit var extension: IntegrationTestExtension + + override fun apply(project: Project) { + extension = project.extensions.create( + "integrationTest", + IntegrationTestExtension::class.java, + project.objects + ) + extension.variantData.set( + AndroidCompactedVariantData( + "demoDevelopmentRelease", + "demo", + "release", + false, + "1.2.3", + listOf("development"), + "" + ) + ) + project.extensions.configure(JavaPluginExtension::class.java) { java -> + java.sourceCompatibility = JavaVersion.VERSION_11 + java.targetCompatibility = JavaVersion.VERSION_11 + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/ProjectType.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/ProjectType.kt new file mode 100644 index 0000000000..84ed273e64 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/ProjectType.kt @@ -0,0 +1,6 @@ +package io.embrace.android.gradle.integration.framework + +enum class ProjectType { + ANDROID, + JVM +} diff --git a/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/SetupInterface.kt b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/SetupInterface.kt new file mode 100644 index 0000000000..19614fda4a --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/main/java/io/embrace/android/gradle/integration/framework/SetupInterface.kt @@ -0,0 +1,37 @@ +package io.embrace.android.gradle.integration.framework + +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeResponse +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import okhttp3.mockwebserver.MockResponse + +class SetupInterface( + private val apiServer: FakeApiServer +) { + + val moshiSerializer = MoshiSerializer() + + /** + * Serializes the object into a string + */ + inline fun serializeRequestBody(obj: T): String { + return moshiSerializer.toJson(obj, T::class.java) + } + + fun enqueueResponse(endpoint: EmbraceEndpoint, response: MockResponse) { + apiServer.enqueueResponse(endpoint, response) + } + + fun SetupInterface.setupMockResponses( + expectedLibs: List, + expectedArchs: List, + expectedVariants: List + ) { + val requestedSymbols = expectedArchs.associateWith { expectedLibs } + val json = serializeRequestBody(NdkUploadHandshakeResponse(requestedSymbols)) + val response = MockResponse().setBody(json) + repeat(expectedVariants.size) { + enqueueResponse(EmbraceEndpoint.NDK_HANDSHAKE, response) + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/ApkDisassembler.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/ApkDisassembler.kt new file mode 100644 index 0000000000..47b6028f84 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/ApkDisassembler.kt @@ -0,0 +1,63 @@ +package io.embrace.android.gradle.integration.framework + +import brut.androlib.ApkDecoder +import brut.androlib.Config +import brut.directory.ExtFile +import org.w3c.dom.Element +import java.io.File +import java.nio.file.Files +import javax.xml.parsers.DocumentBuilderFactory + +/** + * Disassembles an APK using APK tool and inspects the contents. + */ +internal class ApkDisassembler { + + private lateinit var outDir: File + + fun disassembleApk(apkFile: File): DecodedApk { + if (!apkFile.exists()) { + error("APK file does not exist at ${apkFile.path}") + } + + outDir = decodeApk(apkFile) + outDir.exists() + + val stringTable = readStringTable(outDir) + // TODO: dex files + return DecodedApk(stringTable) + } + + private fun decodeApk(apkFile: File): File { + val outDir = Files.createTempDirectory("decoded_apk").toFile() + val config = Config().apply { + isForceDelete = true + } + ApkDecoder(ExtFile(apkFile), config).decode(outDir) + return outDir + } + + private fun readStringTable(outDir: File): Map { + val stringTableFile = File(outDir, "res/values/strings.xml") + if (!stringTableFile.exists()) { + return emptyMap() + } + stringTableFile.inputStream().buffered().use { stream -> + val table = mutableMapOf() + val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val doc = builder.parse(stream) + doc.documentElement.normalize() + val nodes = doc.getElementsByTagName("string") + + for (i in 0 until nodes.length) { + val entry = nodes.item(i) + if (entry is Element) { + val name = entry.getAttribute("name") + val value = entry.textContent + table[name] = value + } + } + return table + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/AssertionInterface.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/AssertionInterface.kt new file mode 100644 index 0000000000..f73b84ced5 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/AssertionInterface.kt @@ -0,0 +1,292 @@ +package io.embrace.android.gradle.integration.framework + +import io.embrace.android.gradle.config.TestMatrix +import io.embrace.android.gradle.network.FormPart +import io.embrace.android.gradle.network.MultipartFormReader +import io.embrace.android.gradle.network.validateBodyApiToken +import io.embrace.android.gradle.network.validateBodyAppId +import io.embrace.android.gradle.network.validateBodyBuildId +import io.embrace.android.gradle.network.validateMappingFile +import io.embrace.android.gradle.plugin.buildreporter.BuildTelemetryRequest +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeRequest +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import okhttp3.mockwebserver.RecordedRequest +import okio.Buffer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import java.io.File + +class AssertionInterface( + private val apiServer: FakeApiServer +) { + + private val multipartFormReader: MultipartFormReader = MultipartFormReader() + + /** + * Fetches a request sent to the mock server with the given endpoint. + */ + fun fetchRequest(endpoint: EmbraceEndpoint): RecordedRequest = fetchRequests(endpoint).single() + + /** + * Fetches all requests sent to the mock server with the given endpoint. + */ + fun fetchRequests(endpoint: EmbraceEndpoint) = apiServer.fetchRequests(endpoint) + + /** + * Reads the parts of a multipart request. + */ + fun readMultipartRequest(request: RecordedRequest): List = + multipartFormReader.read(request) + + /** + * Asserts that common headers (Content-Type, X-EM-AID) are present in the request. + */ + fun assertHeaders(request: RecordedRequest, contentType: String, appId: String?) { + assertEquals(contentType, request.getHeader("Content-Type")?.split(";")?.get(0)) + assertEquals(appId, request.getHeader("X-EM-AID")) + } + + /** + * Deserializes the request body into the given type. + */ + inline fun deserializeRequestBody(request: RecordedRequest): T { + val json = request.body.use(Buffer::readUtf8) + return MoshiSerializer().fromJson(json, T::class.java) + } + + /** + * Deserializes an expected request body from a test fixture file into the given type. + */ + inline fun deserializeExpectedRequestBody( + projectDir: File, + expectedPath: String + ): T { + val json = projectDir.file(expectedPath).readText() + return MoshiSerializer().fromJson(json, T::class.java) + } + + /** + * Deserializes an expected request body from a test fixture file into the given type. + */ + inline fun compareRequestBodyAgainstExpected( + request: RecordedRequest, + projectDir: File, + expectedPath: String + ) { + val observed = deserializeRequestBody(request) + val expected = deserializeExpectedRequestBody(projectDir, expectedPath) + assertEquals(expected, observed) + } + + private fun AssertionInterface.verifyBuildTelemetryRequestContents( + request: RecordedRequest, + expectedVariants: List, + testMatrix: TestMatrix, + validateAppId: Boolean = true + ) { + with(deserializeRequestBody(request)) { + assertNotNull(metadataRequestId) + assertNotNull(pluginVersion) + assertEquals(testMatrix.gradle, gradleVersion) + assertEquals(testMatrix.agp, agpVersion) + assertTrue(checkNotNull(isBuildCacheEnabled)) + assertFalse(checkNotNull(isConfigCacheEnabled)) + assertTrue(checkNotNull(isGradleParallelExecutionEnabled)) + assertNotNull(operatingSystem) + assertNotNull(jreVersion) + assertNotNull(jdkVersion) + assertFalse(checkNotNull(isEdmEnabled)) + + // assert variants match expected list and contain unique build IDs + val variants = checkNotNull(variantMetadata) + variants.forEach { variant -> + assertNotNull(variant.buildId) + assertTrue(expectedVariants.contains(variant.variantName)) + + if (validateAppId) { + assertNotNull(variant.appId) + } + } + assertEquals(variants.size, variants.map { it.buildId }.distinct().count()) + } + } + + /** + * Verifies the contents of a build telemetry request. + */ + fun AssertionInterface.verifyBuildTelemetryRequestSent( + expectedVariants: List, + validateAppId: Boolean = true, + testMatrix: TestMatrix = TestMatrix.MaxVersion, + ) { + val request = fetchRequest(EmbraceEndpoint.BUILD_DATA) + assertHeaders(request, "application/json", null) + + // assert plugin telemetry was sent + verifyBuildTelemetryRequestContents(request, expectedVariants, testMatrix, validateAppId) + } + + /** + * Verifies JVM mapping requests were sent. + */ + fun AssertionInterface.verifyJvmMappingRequestsSent(expectedCount: Int) { + val requests = fetchRequests(EmbraceEndpoint.PROGUARD) + assertEquals(expectedCount, requests.size) + + requests.forEach { request -> + val parts = readMultipartRequest(request) + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + parts[2].validateBodyBuildId() + parts[3].validateMappingFile("mapping.txt") + } + } + + /** + * Verifies no JVM mapping requests were sent. + */ + fun AssertionInterface.verifyNoRequestsSent(endpoint: EmbraceEndpoint) { + val requests = fetchRequests(endpoint) + assertEquals(0, requests.size) + } + + fun AssertionInterface.verifyNoHandshakes() { + val handshakes = fetchRequests(EmbraceEndpoint.NDK_HANDSHAKE).map { + deserializeRequestBody(it) + } + assertEquals(0, handshakes.size) + } + + fun AssertionInterface.verifyHandshakes( + expectedLibs: List, + expectedArchs: List, + expectedVariants: List + ) { + val handshakes = fetchRequests(EmbraceEndpoint.NDK_HANDSHAKE).map { + deserializeRequestBody(it) + } + + expectedVariants.forEach { variant -> + val handshake = handshakes.find { it.variant == variant } + verifyNdkHandshake( + handshake ?: error("Handshake not found for variant: $variant"), + variant, + expectedLibs, + expectedArchs + ) + } + } + + private fun verifyNdkHandshake( + handshake: NdkUploadHandshakeRequest, + expectedVariantName: String, + expectedLibs: List, + expectedArchs: List + ) { + assertEquals(expectedVariantName, handshake.variant) + assertEquals(IntegrationTestDefaults.APP_ID, handshake.appId) + assertEquals(IntegrationTestDefaults.API_TOKEN, handshake.apiToken) + + val symbols = handshake.archSymbols + assertEquals(expectedArchs.sorted(), symbols.keys.toList().sorted()) + + symbols.forEach { entry -> + assertEquals(expectedLibs.sorted(), entry.value.keys.toList().sorted()) + + expectedLibs.forEach { lib -> + assertNotNull(entry.value[lib]) + } + } + } + + fun AssertionInterface.verifyNoUploads() { + val uploads = fetchRequests(EmbraceEndpoint.NDK).map(::readMultipartRequest) + assertEquals(0, uploads.size) + } + + fun AssertionInterface.verifyUploads( + expectedLibs: List, + expectedArchs: List, + expectedVariants: List + ) { + val uploads = fetchRequests(EmbraceEndpoint.NDK).map(::readMultipartRequest) + val expectedLibsSize = expectedLibs.size + val expectedArchsSize = expectedArchs.size + val expectedVariantsSize = expectedVariants.size + // Verify that there are expectedLibs * expectedArchs * expectedVariants uploads + assertEquals(expectedLibsSize * expectedArchsSize * expectedVariantsSize, uploads.size) + + // Validate library uploads (one for every combination of architecture and variant) + expectedLibs.forEach { + assertUploadsCount( + uploads, + fieldIndex = 6, + expectedData = it, + count = expectedArchsSize * expectedVariantsSize + ) + validateUploadedFile(uploads, it) + } + + // Validate variants (one for every combination of architecture and library) + expectedVariants.forEach { + assertUploadsCount( + uploads, + fieldIndex = 3, + expectedData = it, + count = expectedLibsSize * expectedArchsSize + ) + } + + // Validate architectures (one for every combination of library and variant) + expectedArchs.forEach { arch -> + assertUploadsCount( + uploads, + fieldIndex = 4, + expectedData = arch, + count = expectedLibsSize * expectedVariantsSize + ) + } + + // Validate each upload has correct appId and apiToken + uploads.forEach { upload -> verifyNdkSymbolUpload(upload) } + } + + /** + * Filters the list of uploads given a specific field index and the expected data for that index. + * Asserts that the filtered list has the expected number of elements. + */ + private fun assertUploadsCount( + uploads: List>, + fieldIndex: Int, + expectedData: String, + count: Int + ) { + val filteredUploads = uploads.filter { it[fieldIndex].data == expectedData } + assertEquals( + "Expected $count uploads with $expectedData in field $fieldIndex", + count, + filteredUploads.size + ) + } + + /** + * Verifies that the uploads are sending a non-empty file, and the content disposition contains the expected library name. + */ + private fun validateUploadedFile(uploads: List>, expectedLib: String) { + val filteredUploads = uploads.filter { + it[5].data == expectedLib + } + filteredUploads.forEach { + assertFalse(it[6].data.isNullOrEmpty()) + assertTrue(it[6].contentDisposition.contains(expectedLib)) + } + } + + private fun verifyNdkSymbolUpload(parts: List) { + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/DecodedApk.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/DecodedApk.kt new file mode 100644 index 0000000000..48460469f8 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/DecodedApk.kt @@ -0,0 +1,9 @@ +package io.embrace.android.gradle.integration.framework + +internal class DecodedApk( + private val stringTable: Map +) { + fun getStringResource(name: String): String? { + return stringTable[name] + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/PluginIntegrationTestRule.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/PluginIntegrationTestRule.kt new file mode 100644 index 0000000000..367bfc2857 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/framework/PluginIntegrationTestRule.kt @@ -0,0 +1,267 @@ +package io.embrace.android.gradle.integration.framework + +import io.embrace.android.gradle.config.TestMatrix +import okhttp3.mockwebserver.MockWebServer +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Assert.assertEquals +import org.junit.rules.ExternalResource +import java.io.File +import java.nio.file.Files +import java.util.Properties +import java.util.concurrent.atomic.AtomicInteger + +class PluginIntegrationTestRule : ExternalResource() { + + private val uniqueDirName: AtomicInteger = AtomicInteger(0) + + private var tmpDir: File? = null + private lateinit var server: MockWebServer + private lateinit var apiServer: FakeApiServer + private lateinit var assertionInterface: AssertionInterface + private lateinit var setupInterface: SetupInterface + private lateinit var baseUrl: String + + override fun before() { + apiServer = FakeApiServer() + setupInterface = SetupInterface(apiServer) + assertionInterface = AssertionInterface(apiServer) + server = MockWebServer().apply { + dispatcher = apiServer + start() + } + baseUrl = server.url("api").toString() + } + + override fun after() { + tmpDir?.deleteRecursively() + server.shutdown() + } + + /** + * Runs a simple integration test for a single Gradle task. Generally speaking most test cases + * can be written this way if you need to run something once, then assert that the file system + * was changed appropriately. If you need to run multiple builds for one test, e.g. in order + * to test that a task handles caching correctly, then you can simply call this function + * multiple times in a single JUnit test case. + */ + @Suppress("LongParameterList") + fun runTest( + + /** + * The path to the fixture directory, relative to embrace-gradle-plugin-integration-tests/fixtures. + */ + fixture: String, + + /** + * The name of the task to run. + */ + task: String = "testTask", + + /** + * The versions of Gradle/AGP to use. + */ + testMatrix: TestMatrix = TestMatrix.MaxVersion, + + /** + * The type of project. For Android projects, this class will automatically add + * additional MVP files such as an AndroidManifest.xml, as this reduces + * the boilerplate required in test fixtures. + */ + projectType: ProjectType = ProjectType.JVM, + + /** + * The path to the android project root, relative to the parent folder. This is used on hosted SDK tests, where you also need + * to set up the parent folder of the android project. + * + * For example, if the android project is located at `fixtures/react-native-android/android`, + * then the value of this parameter should be "android". + */ + androidProjectRoot: String? = null, + + /** + * Extra arguments to add on top of the task name. + */ + additionalArgs: List = emptyList(), + + /** + * The expected outcome of the task. + */ + expectedOutcome: TaskOutcome = TaskOutcome.SUCCESS, + + /** + * Whether a debugger should be attached to the TestKit build. This stops the build + * proceeding until a remote debugger is attached. + */ + attachDebugger: Boolean = false, + + /** + * Custom assertions to run after the build has finished. The assertions should focus on + * the state of the file system. + */ + assertions: AssertionInterface.(projectDir: File) -> Unit, + + /** + * Custom setup to run before the build is executed. This could be used to set up a certain file system state, or even + * assert that some preconditions are met before the build is run. + */ + setup: SetupInterface.(projectDir: File) -> Unit = {}, + ) { + // only copy the test fixture if it hasn't already been copied as this function can be + // invoked multiple times per test case + val dir = tmpDir ?: prepareTempDirectory(fixture, projectType, androidProjectRoot) + tmpDir = dir + val args = prepareCommandArgs( + task, + projectType, + baseUrl, + additionalArgs, + attachDebugger, + testMatrix + ) + + // run any preconditions before the build + setupInterface.setup(dir) + val result = executeGradleBuild(args, testMatrix.gradle) + + // run assertions on the build outcome + assertTaskOutcome(result, task, expectedOutcome) + assertionInterface.assertions(dir) + } + + /** + * Copies the test fixture to a temporary directory. This ensures a clean build happens each + * time and there's no possibility of side effects from previous runs. + */ + private fun prepareTempDirectory( + fixture: String, + projectType: ProjectType, + androidProjectRoot: String? + ): File { + val srcDir = File("fixtures/$fixture") + if (!srcDir.exists()) { + error("Fixture '${srcDir.absolutePath}' not found.") + } + val tmpDir = Files.createTempDirectory("${uniqueDirName.incrementAndGet()}").toFile() + if (!tmpDir.exists()) { + error("Failed to create temporary directory $tmpDir.") + } + srcDir.copyRecursively(tmpDir, overwrite = true) + val androidProjectDir = androidProjectRoot?.let { File(tmpDir, it) } ?: tmpDir + createProjectBoilerplate(androidProjectDir, projectType) + return androidProjectDir + } + + private fun createProjectBoilerplate(projectDir: File, projectType: ProjectType) { + // create an empty settings.gradle.kts file - required for Gradle TestKit + copyFakeFile(projectDir, "settings.gradle.kts", "fakesettings.gradle.kts") + + // create a minimal AndroidManifest.xml and embrace-config.json + if (projectType == ProjectType.ANDROID) { + copyFakeFile(projectDir, "src/main/AndroidManifest.xml", "FakeAndroidManifest.xml") + copyFakeFile(projectDir, "src/main/embrace-config.json", "fake-embrace-config.json") + } + } + + private fun copyFakeFile(projectDir: File, dst: String, resName: String) { + File(projectDir, dst).apply { + if (exists()) { + // don't overwrite an existing file if a test fixture chooses to supply something + return + } + parentFile.mkdirs() + outputStream().buffered().use { + val classLoader = checkNotNull(this@PluginIntegrationTestRule.javaClass.classLoader) + checkNotNull(classLoader.getResourceAsStream(resName)).use { resourceStream -> + resourceStream.buffered().copyTo(it) + } + } + } + } + + /** + * Executes a Gradle build using TestKit. + */ + private fun executeGradleBuild( + args: List, + gradleVersion: String + ): BuildResult = GradleRunner.create() + .withProjectDir(tmpDir) + .withArguments(args) + .withGradleVersion(gradleVersion) + .forwardStdOutput(System.out.writer()) + .forwardStdError(System.err.writer()) + .build() + + /** + * Adds some additional arguments to the gradle build for better performance of the test suite. + */ + private fun prepareCommandArgs( + task: String, + projectType: ProjectType, + baseUrl: String?, + additionalArgs: List, + attachDebugger: Boolean, + testMatrix: TestMatrix, + ): List { + val args = mutableListOf(task) + args.addAll(additionalArgs) + args.addAll( + listOf( + "--stacktrace", + "-Dorg.gradle.daemon=false", + "-Dorg.gradle.parallel=true", + "-Dorg.gradle.caching=true", + "-Dorg.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g", + "-Dorg.gradle.java.home=${testMatrix.jdk.path}", + "-Pembrace.baseUrl=$baseUrl", + "-Pagp_version=${testMatrix.agp}", + "-Pkotlin_version=${testMatrix.kotlin}", + "-Pplugin_snapshot_version=${loadSnapshotVersion()}", + ) + ) + // Android projects currently require this flag to compile + if (projectType == ProjectType.ANDROID) { + args.add("-Pandroid.useAndroidX=true") + } + if (attachDebugger) { + println( + "Waiting for debugger to attach. You need to run the remote debugging " + + "configuration from Android Studio for this test case to continue. Please " + + "see this module's README.md for further details." + ) + args.addAll( + listOf( + "-Dorg.gradle.debug=true", + ) + ) + } + return args + } + + private fun loadSnapshotVersion(): String { + val rootDir = File(System.getProperty("user.dir")).parentFile + val file = File(rootDir, "gradle.properties") + file.inputStream().buffered().use { + Properties().apply { + load(it) + return getProperty("version") + } + } + } + + /** + * Asserts the task executed with the expected outcome. Generally speaking this will be SUCCESS. + */ + private fun assertTaskOutcome( + result: BuildResult, + task: String, + expectedOutcome: TaskOutcome + ) { + val buildTask = result.tasks.singleOrNull { it.path == ":$task" || it.path == ":app:$task" } + ?: error("Task '$task' not found in result.") + val outcome = buildTask.outcome + assertEquals(expectedOutcome, outcome) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AgpSupportTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AgpSupportTest.kt new file mode 100644 index 0000000000..415a6f8cb9 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AgpSupportTest.kt @@ -0,0 +1,47 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.config.TestMatrix +import io.embrace.android.gradle.config.TestMatrix.MaxVersion +import io.embrace.android.gradle.config.TestMatrix.MiddleVersion +import io.embrace.android.gradle.config.TestMatrix.MinVersion +import io.embrace.android.gradle.config.TestMatrix.NewerVersion +import io.embrace.android.gradle.config.TestMatrix.OlderVersion +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AgpSupportTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `minimum supported version`() = runTest(MinVersion) + + @Test + fun `older version`() = runTest(OlderVersion) + + @Test + fun `middle version`() = runTest(MiddleVersion) + + @Test + fun `newer version`() = runTest(NewerVersion) + + @Test + fun `maximum supported version`() = runTest(MaxVersion) + + private fun runTest(testMatrix: TestMatrix) { + rule.runTest( + fixture = "android-version-support", + task = "assembleRelease", + testMatrix = testMatrix, + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(listOf("debug", "release"), testMatrix = testMatrix) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeProductFlavorTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeProductFlavorTest.kt new file mode 100644 index 0000000000..4adea6fd6b --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeProductFlavorTest.kt @@ -0,0 +1,119 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AndroidBuildTypeProductFlavorTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("demoDebug", "fullDebug", "demoRelease", "fullRelease", "demoCustom", "fullCustom") + + @Test + fun assemble() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(4) + } + ) + } + + @Test + fun assembleProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleDemo", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleBuildType() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleBuildTypeProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleFullCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundle() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundle", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(4) + } + ) + } + + @Test + fun bundleProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleDemo", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleBuildType() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleBuildTypeProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleFullCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeTest.kt new file mode 100644 index 0000000000..fab02076c6 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidBuildTypeTest.kt @@ -0,0 +1,67 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AndroidBuildTypeTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("debug", "release", "custom") + + @Test + fun assemble() { + rule.runTest( + fixture = "android-build-types", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleBuildType() { + rule.runTest( + fixture = "android-build-types", + task = "assembleCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundle() { + rule.runTest( + fixture = "android-build-types", + task = "bundle", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleBuildType() { + rule.runTest( + fixture = "android-build-types", + task = "bundleCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidFlavorDimensionsTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidFlavorDimensionsTest.kt new file mode 100644 index 0000000000..d0fc47becc --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidFlavorDimensionsTest.kt @@ -0,0 +1,119 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AndroidFlavorDimensionsTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("demoDebug", "fullDebug", "demoRelease", "fullRelease", "demoCustom", "fullCustom") + + @Test + fun assemble() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(4) + } + ) + } + + @Test + fun assembleProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleDemo", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleBuildType() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleBuildTypeProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "assembleFullCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundle() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundle", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(4) + } + ) + } + + @Test + fun bundleProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleDemo", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleBuildType() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleBuildTypeProductFlavor() { + rule.runTest( + fixture = "android-build-types-product-flavors", + task = "bundleFullCustom", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNdkTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNdkTest.kt new file mode 100644 index 0000000000..cf77a5d723 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNdkTest.kt @@ -0,0 +1,248 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.ApkDisassembler +import io.embrace.android.gradle.integration.framework.BundleToolApkBuilder +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import io.embrace.android.gradle.integration.utils.NdkSymbols +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import okio.ByteString.Companion.decodeBase64 +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import java.io.File + +class AndroidNdkTest { + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val defaultExpectedVariants = listOf("debug", "release") + private val defaultExpectedLibs = listOf("libemb-donuts.so", "libemb-crisps.so") + private val defaultExpectedArchs = listOf("x86_64", "x86", "armeabi-v7a", "arm64-v8a") + + @Test + fun buildCMake() { + rule.runTest( + fixture = "android-cmake", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + defaultExpectedLibs, + defaultExpectedArchs, + defaultExpectedVariants + ) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyUploads(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + } + ) + } + + @Test + fun buildNdkBuild() { + rule.runTest( + fixture = "android-ndk-build", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + defaultExpectedLibs, + defaultExpectedArchs, + defaultExpectedVariants + ) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyUploads(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + } + ) + } + + // When a 3rd party dependency has native libraries, but the project has no externalNativeBuild block, we don't send symbols (yet). + @Test + fun `don't send 3rd party native library symbols when the project has no native libraries`() { + rule.runTest( + fixture = "android-3rd-party-symbols", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + defaultExpectedLibs, + defaultExpectedArchs, + defaultExpectedVariants + ) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyNoHandshakes() + verifyNoUploads() + } + ) + } + + // If besides the project .so files, there are also 3rd party libraries with native code, we should send the symbols for them as well. + @Test + fun `send local and 3rd party library symbols`() { + val expectedLibs = defaultExpectedLibs + "libemb-asado.so" + rule.runTest( + fixture = "android-local-and-3rd-party-symbols", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses(expectedLibs, defaultExpectedArchs, defaultExpectedVariants) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(expectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyUploads(expectedLibs, defaultExpectedArchs, defaultExpectedVariants) + } + ) + } + + @Test + fun `don't upload symbols if not requested by the backend`() { + rule.runTest( + fixture = "android-cmake", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + expectedLibs = emptyList(), + expectedArchs = emptyList(), + expectedVariants = defaultExpectedVariants + ) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyNoUploads() + } + ) + } + + @Test + fun `partial upload when some symbols are not requested by the backend`() { + val expectedLibs = listOf("libemb-donuts.so") + val expectedArchs = listOf("arm64-v8a") + rule.runTest( + fixture = "android-cmake", + task = "build", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + expectedLibs = expectedLibs, + expectedArchs = expectedArchs, + expectedVariants = defaultExpectedVariants + ) + }, + assertions = { + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyUploads(expectedLibs, expectedArchs, defaultExpectedVariants) + } + ) + } + + @Test + fun `symbols are injected into the APK`() { + rule.runTest( + fixture = "android-cmake", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + defaultExpectedLibs, + defaultExpectedArchs, + defaultExpectedVariants + ) + }, + assertions = { projectDir -> + val apk = findArtifact(projectDir, "build/outputs/apk/release/", ".apk") + verifyNdkApkSymbolsInjection(apk) + } + ) + } + + @Test + fun `symbols are injected into the bundle`() { + rule.runTest( + fixture = "android-cmake", + task = "bundleRelease", + projectType = ProjectType.ANDROID, + setup = { + setupMockResponses( + defaultExpectedLibs, + defaultExpectedArchs, + defaultExpectedVariants + ) + }, + assertions = { projectDir -> + verifyBundleSymbolsInjection(projectDir) + } + ) + } + + @Test + fun `ndk disabled test`() { + rule.runTest( + fixture = "android-cmake-ndk-disabled", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { projectDir -> + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyNoHandshakes() + verifyNoUploads() + verifyJvmMappingRequestsSent(1) + val apk = findArtifact(projectDir, "build/outputs/apk/release/", ".apk") + verifyNoSymbolsInjected(apk) + } + ) + } + + private fun verifyNoSymbolsInjected(apkFile: File) { + val decodedApk = ApkDisassembler().disassembleApk(apkFile) + val resourceName = "emb_ndk_symbols" + assertNull(decodedApk.getStringResource(resourceName)) + } + + private fun verifyNdkApkSymbolsInjection(apkFile: File) { + val decodedApk = ApkDisassembler().disassembleApk(apkFile) + val resourceName = "emb_ndk_symbols" + val symbols = decodedApk.getStringResource(resourceName) + ?: error("Resource named '$resourceName' not found") + validateBase64Symbols(symbols) + } + + private fun verifyBundleSymbolsInjection(projectDir: File) { + val bundle = findArtifact(projectDir, "build/outputs/bundle/release/", ".aab") + val apkFile = BundleToolApkBuilder().generateApkFromBundle(bundle) + verifyNdkApkSymbolsInjection(apkFile) + } + + private fun validateBase64Symbols(base64Symbols: String) { + val symbolString = + base64Symbols.decodeBase64()?.utf8() ?: error("Failed to decode base64 symbols") + + val symbolsMap = MoshiSerializer().fromJson(symbolString, NdkSymbols::class.java).symbols + ?: error("Failed to deserialize symbols") + + defaultExpectedArchs.forEach { arch -> + defaultExpectedLibs.forEach { lib -> + assertTrue(symbolsMap[arch]?.containsKey(lib) ?: false) + } + } + } + + private fun findArtifact(projectDir: File, buildDir: String, suffix: String): File { + return File(projectDir, buildDir) + .listFiles { _, name -> name.endsWith(suffix) } + ?.single() + ?: error("File not found") + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNestedTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNestedTest.kt new file mode 100644 index 0000000000..110069e97b --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidNestedTest.kt @@ -0,0 +1,28 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AndroidNestedTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("debug", "release") + + @Test + fun assembleRelease() { + rule.runTest( + fixture = "android-nested", + task = "app:assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidProductFlavorTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidProductFlavorTest.kt new file mode 100644 index 0000000000..ced89b5025 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidProductFlavorTest.kt @@ -0,0 +1,67 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class AndroidProductFlavorTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("demoDebug", "fullDebug", "demoRelease", "fullRelease") + + @Test + fun assemble() { + rule.runTest( + fixture = "android-product-flavors", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun assembleProductFlavor() { + rule.runTest( + fixture = "android-product-flavors", + task = "assembleDemo", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundle() { + rule.runTest( + fixture = "android-product-flavors", + task = "bundle", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(2) + } + ) + } + + @Test + fun bundleProductFlavor() { + rule.runTest( + fixture = "android-product-flavors", + task = "bundleFull", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidSimpleTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidSimpleTest.kt new file mode 100644 index 0000000000..9e1c083cd7 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/AndroidSimpleTest.kt @@ -0,0 +1,107 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Rule +import org.junit.Test + +class AndroidSimpleTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("debug", "release") + + @Test + fun assemble() { + rule.runTest( + fixture = "android-simple", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun assembleRelease() { + rule.runTest( + fixture = "android-simple", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun assembleDebug() { + rule.runTest( + fixture = "android-simple", + task = "assembleDebug", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyNoRequestsSent(EmbraceEndpoint.PROGUARD) + } + ) + } + + @Test + fun bundle() { + rule.runTest( + fixture = "android-simple", + task = "bundle", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundleRelease() { + rule.runTest( + fixture = "android-simple", + task = "bundleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } + + @Test + fun bundleDebug() { + rule.runTest( + fixture = "android-simple", + task = "bundleDebug", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyNoRequestsSent(EmbraceEndpoint.PROGUARD) + } + ) + } + + @Test + fun build() { + rule.runTest( + fixture = "android-simple", + task = "build", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/CompressedFileUploadTaskIntegrationTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/CompressedFileUploadTaskIntegrationTest.kt new file mode 100644 index 0000000000..a23b401783 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/CompressedFileUploadTaskIntegrationTest.kt @@ -0,0 +1,79 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.AssertionInterface +import io.embrace.android.gradle.integration.framework.IntegrationTestDefaults +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.file +import io.embrace.android.gradle.network.FormPart +import io.embrace.android.gradle.network.validateBodyApiToken +import io.embrace.android.gradle.network.validateBodyAppId +import io.embrace.android.gradle.network.validateBodyBuildId +import io.embrace.android.gradle.network.validateMappingFile +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class CompressedFileUploadTaskIntegrationTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `upload compressed file`() { + var expectedFileContents: String? = null + rule.runTest( + fixture = "file-upload-simple", + setup = { projectDir -> + assertTrue(projectDir.file("input.txt").exists()) + expectedFileContents = projectDir.file("input.txt").readText() + }, + assertions = { + assertFileUploaded(expectedFileContents, EmbraceEndpoint.PROGUARD) + } + ) + } + + @Test + fun `skips upload when file is missing`() { + rule.runTest( + fixture = "file-upload-missing-input", + expectedOutcome = TaskOutcome.NO_SOURCE, + assertions = { + verifyNoRequestsSent(EmbraceEndpoint.PROGUARD) + } + ) + } + + @Test + fun `configure endpoint`() { + var expectedFileContents: String? = null + rule.runTest( + fixture = "file-upload-set-endpoint", + setup = { projectDir -> + assertTrue(projectDir.file("input.txt").exists()) + expectedFileContents = projectDir.file("input.txt").readText() + }, + assertions = { + assertFileUploaded(expectedFileContents, EmbraceEndpoint.NDK) + } + ) + } + + private fun AssertionInterface.assertFileUploaded(expectedFileContents: String?, endpoint: EmbraceEndpoint) { + val request = fetchRequest(endpoint) + assertHeaders(request, "multipart/form-data", "abcde") + + val parts: List = readMultipartRequest(request) + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + parts[2].validateBodyBuildId() + assertEquals(IntegrationTestDefaults.BUILD_ID, parts[2].data) + + parts[3].validateMappingFile("my-filename.txt") + assertEquals(expectedFileContents, parts[3].data) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ConfigCacheTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ConfigCacheTest.kt new file mode 100644 index 0000000000..5f55a11f5f --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ConfigCacheTest.kt @@ -0,0 +1,31 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class ConfigCacheTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + /** + * Test that the assemble task works with configuration cache enabled without throwing an error + */ + @Test + fun assembleRelease() { + rule.runTest( + fixture = "android-simple", + task = "assembleRelease", + additionalArgs = listOf( + "-Dorg.gradle.configuration-cache=true", + "-Dorg.gradle.configuration-cache.problems=fail", + ), + projectType = ProjectType.ANDROID, + assertions = { + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/DisablePluginTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/DisablePluginTest.kt new file mode 100644 index 0000000000..f8cb2b7692 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/DisablePluginTest.kt @@ -0,0 +1,28 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import org.junit.Rule +import org.junit.Test + +class DisablePluginTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val variants = listOf("demoDebug", "fullDebug", "demoRelease", "fullRelease") + + @Test + fun `plugin entirely disabled for one build variant`() { + rule.runTest( + fixture = "android-disable-product-flavor", + task = "assemble", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(variants) + verifyJvmMappingRequestsSent(1) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/FileCompressionTaskIntegrationTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/FileCompressionTaskIntegrationTest.kt new file mode 100644 index 0000000000..51a6fe60ee --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/FileCompressionTaskIntegrationTest.kt @@ -0,0 +1,54 @@ +package io.embrace.android.gradle.integration.testcases + +import com.github.luben.zstd.ZstdInputStream +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.buildFile +import io.embrace.android.gradle.integration.framework.file +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class FileCompressionTaskIntegrationTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `copies input file and compresses it to output file`() { + rule.runTest( + fixture = "file-compression-simple", + setup = { projectDir -> + assertTrue(projectDir.file("input.txt").exists()) + assertFalse(projectDir.buildFile("mapping.txt").exists()) + }, + assertions = { projectDir -> + // assert the task created a gzipped file + val dst = projectDir.buildFile("mapping.txt") + assertTrue(dst.exists() && dst.length() > 0) + + val output = ZstdInputStream(dst.inputStream()).bufferedReader().use { + it.readText() + } + assertEquals("Hello, world!", output) + val input = projectDir.file("input.txt").readText() + assertEquals(input, output) + } + ) + } + + @Test + fun `skips compression when input file is missing`() { + rule.runTest( + fixture = "file-compression-missing-input", + expectedOutcome = TaskOutcome.NO_SOURCE, + assertions = { projectDir -> + assertFalse(projectDir.file("mapping.txt").exists()) + assertFalse(projectDir.buildFile("mapping.txt").exists()) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/MissingConfigFileTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/MissingConfigFileTest.kt new file mode 100644 index 0000000000..d2bbef1449 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/MissingConfigFileTest.kt @@ -0,0 +1,34 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Rule +import org.junit.Test +import java.io.File + +class MissingConfigFileTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun assembleRelease() { + rule.runTest( + fixture = "android-simple", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + setup = { projectDir -> + File(projectDir, "src/main/embrace-config.json").delete() + }, + assertions = { + val endpoints = EmbraceEndpoint.values().filter { it != EmbraceEndpoint.BUILD_DATA } + endpoints.forEach { + verifyNoRequestsSent(it) + } + verifyBuildTelemetryRequestSent(listOf("debug", "release"), validateAppId = false) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/NdkUploadTaskIntegrationTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/NdkUploadTaskIntegrationTest.kt new file mode 100644 index 0000000000..a8149573c1 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/NdkUploadTaskIntegrationTest.kt @@ -0,0 +1,40 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.buildFile +import io.embrace.android.gradle.integration.framework.file +import io.embrace.android.gradle.network.NdkHandshakeRequestBody +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class NdkUploadTaskIntegrationTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `ndk upload`() { + rule.runTest( + fixture = "ndk-upload-simple", + assertions = { projectDir -> + // 1. assert that the symbols were injected as a string resource + val symbols = + projectDir.buildFile("generated-embrace-resources/values/ndk_symbols.xml") + val expectedResXml = projectDir.file("expected/ndk_symbols.xml").readText() + assertEquals(expectedResXml, symbols.readText()) + + // 2. assert that a handshake request took place + val request = fetchRequest(EmbraceEndpoint.NDK_HANDSHAKE) + assertHeaders(request, "application/json", "abcde") + compareRequestBodyAgainstExpected( + request, + projectDir, + "expected/handshake.json" + ) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/PluginTelemetryTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/PluginTelemetryTest.kt new file mode 100644 index 0000000000..49d07ff907 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/PluginTelemetryTest.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Rule +import org.junit.Test + +class PluginTelemetryTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `telemetry sent by plugin`() { + rule.runTest( + fixture = "android-simple", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyBuildTelemetryRequestSent(listOf("debug", "release")) + } + ) + } + + @Test + fun `telemetry capture disabled`() { + rule.runTest( + fixture = "android-simple", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + additionalArgs = listOf("-Pembrace.disableCollectBuildData=true"), + assertions = { + verifyNoRequestsSent(EmbraceEndpoint.BUILD_DATA) + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ReactNativeAndroidTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ReactNativeAndroidTest.kt new file mode 100644 index 0000000000..95a1a6198e --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/ReactNativeAndroidTest.kt @@ -0,0 +1,79 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.ApkDisassembler +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import org.junit.Assert.assertNotNull +import org.junit.Rule +import org.junit.Test +import java.io.File + +class ReactNativeAndroidTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + private val defaultExpectedVariants = listOf("debug", "release") + private val defaultExpectedLibs = listOf( + "libappmodules.so", + "libc++_shared.so", + "libembrace-native.so", + "libfbjni.so", + "libhermes.so", + "libhermestooling.so", + "libimagepipeline.so", + "libjsi.so", + "libnative-filters.so", + "libnative-imagetranscoder.so", + "libreactnative.so" + ) + + private val defaultExpectedArchs = listOf("x86_64", "x86", "armeabi-v7a", "arm64-v8a") + + @Test + fun `react native ndk upload test`() { + val handshakeLibs = listOf( + "libfbjni.so", + "libhermes.so", + "libhermestooling.so", + ) + val handshakeArchs = listOf("arm64-v8a", "armeabi-v7a") + rule.runTest( + fixture = "react-native-android", + androidProjectRoot = "android", + task = "build", + setup = { projectDir -> + installNodeModules(projectDir) + setupMockResponses(handshakeLibs, handshakeArchs, defaultExpectedVariants) + }, + assertions = { projectDir -> + verifyBuildTelemetryRequestSent(defaultExpectedVariants) + verifyHandshakes(defaultExpectedLibs, defaultExpectedArchs, defaultExpectedVariants) + verifyUploads(handshakeLibs, handshakeArchs, defaultExpectedVariants) + + val filename = "app/build/outputs/apk/release/app-release.apk" + verifyBundleIdInjection(File(projectDir, filename)) + } + ) + } + + private fun verifyBundleIdInjection(apkFile: File) { + val decodedApk = ApkDisassembler().disassembleApk(apkFile) + val bundleId = decodedApk.getStringResource("emb_rn_bundle_id") + assertNotNull(bundleId) + } + + private fun installNodeModules(projectDir: File) { + val process = ProcessBuilder("npm", "install") + .directory(projectDir) + .redirectErrorStream(true) // Merges stderr into stdout + .start() + + val output = process.inputStream.bufferedReader().use { it.readText() } + val exitCode = process.waitFor() + + if (exitCode != 0) { + error("npm install failed with exit code $exitCode\n$output") + } + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/RnSourcemapTaskIntegrationTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/RnSourcemapTaskIntegrationTest.kt new file mode 100644 index 0000000000..9d40f3d8f8 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/RnSourcemapTaskIntegrationTest.kt @@ -0,0 +1,53 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.IntegrationTestDefaults +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.buildFile +import io.embrace.android.gradle.integration.framework.file +import io.embrace.android.gradle.network.FormPart +import io.embrace.android.gradle.network.validateBodyApiToken +import io.embrace.android.gradle.network.validateBodyAppId +import io.embrace.android.gradle.network.validateBodyBuildId +import io.embrace.android.gradle.network.validateMappingFile +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class RnSourcemapTaskIntegrationTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `react native upload`() { + rule.runTest( + fixture = "rn-upload-simple", + assertions = { projectDir -> + // 1. assert that RN bundle ID was injected as a string resource + val symbols = + projectDir.buildFile("generated-embrace-resources/values/rn_sourcemap.xml") + val expectedResXml = projectDir.file("expected/rn_sourcemap.xml").readText() + assertEquals(expectedResXml, symbols.readText()) + + // 2. assert that the task output was the bundle + sourcemap file zipped as JSON + val output = projectDir.buildFile("output") + assertTrue(output.exists() && output.length() > 0) + + // 3. assert that a request took place + val request = fetchRequest(EmbraceEndpoint.SOURCE_MAP) + assertHeaders(request, "multipart/form-data", "abcde") + + // 4. assert the multipart form data contains bundle info + val parts: List = readMultipartRequest(request) + assertEquals(4, parts.size) + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + parts[2].validateBodyBuildId() + parts[3].validateMappingFile("sourcemap.json") + } + ) + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/UnityTest.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/UnityTest.kt new file mode 100644 index 0000000000..95dfba999d --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/testcases/UnityTest.kt @@ -0,0 +1,68 @@ +package io.embrace.android.gradle.integration.testcases + +import io.embrace.android.gradle.integration.framework.AssertionInterface +import io.embrace.android.gradle.integration.framework.IntegrationTestDefaults +import io.embrace.android.gradle.integration.framework.PluginIntegrationTestRule +import io.embrace.android.gradle.integration.framework.ProjectType +import io.embrace.android.gradle.network.validateBodyApiToken +import io.embrace.android.gradle.network.validateBodyAppId +import io.embrace.android.gradle.network.validateBodyBuildId +import io.embrace.android.gradle.network.validateMappingFile +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import org.junit.Rule +import org.junit.Test + +/** + * Tests that IL2CPP mapping files are uploaded in a Unity project. Building an actual Unity + * project would take an unacceptably long time for a unit test, so we fake the important details + * in our test fixture: + * + * 1. The project uses the NDK (as Unity apps always use NDK) + * 2. embrace.uploadIl2CppMappingFiles=true is set in the project's gradle.properties. Our Unity + * SDK writes this value usually. + * 3. A file exists at unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols from + * the project root. Our Unity SDK copies this file into the Gradle project. + */ +class UnityTest { + + @Rule + @JvmField + val rule: PluginIntegrationTestRule = PluginIntegrationTestRule() + + @Test + fun `build unity project`() { + rule.runTest( + fixture = "unity-fake-project", + task = "assembleRelease", + projectType = ProjectType.ANDROID, + assertions = { + verifyLineMapRequestSent() + verifyMethodMapRequestSent() + } + ) + } + + /** + * Verifies the LineNumberMappings.json file is sent + */ + private fun AssertionInterface.verifyLineMapRequestSent() { + val request = fetchRequests(EmbraceEndpoint.LINE_MAP).single() + val parts = readMultipartRequest(request) + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + parts[2].validateBodyBuildId() + parts[3].validateMappingFile("LineNumberMappings.json") + } + + /** + * Verifies the MethodMap.tsv file is sent + */ + private fun AssertionInterface.verifyMethodMapRequestSent() { + val request = fetchRequests(EmbraceEndpoint.METHOD_MAP).single() + val parts = readMultipartRequest(request) + parts[0].validateBodyAppId(IntegrationTestDefaults.APP_ID) + parts[1].validateBodyApiToken(IntegrationTestDefaults.API_TOKEN) + parts[2].validateBodyBuildId() + parts[3].validateMappingFile("MethodMap.tsv") + } +} diff --git a/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/utils/NdkSymbols.kt b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/utils/NdkSymbols.kt new file mode 100644 index 0000000000..a71aa1cf0b --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/java/io/embrace/android/gradle/integration/utils/NdkSymbols.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.integration.utils + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NdkSymbols( + val symbols: Map>? = null +) diff --git a/embrace-gradle-plugin-integration-tests/src/test/resources/FakeAndroidManifest.xml b/embrace-gradle-plugin-integration-tests/src/test/resources/FakeAndroidManifest.xml new file mode 100644 index 0000000000..50e0a262fc --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/resources/FakeAndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/embrace-gradle-plugin-integration-tests/src/test/resources/fake-embrace-config.json b/embrace-gradle-plugin-integration-tests/src/test/resources/fake-embrace-config.json new file mode 100644 index 0000000000..7cda90d4de --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/resources/fake-embrace-config.json @@ -0,0 +1,4 @@ +{ + "app_id": "abcde", + "api_token": "12345123451234512345123451234512" +} \ No newline at end of file diff --git a/embrace-gradle-plugin-integration-tests/src/test/resources/fakesettings.gradle.kts b/embrace-gradle-plugin-integration-tests/src/test/resources/fakesettings.gradle.kts new file mode 100644 index 0000000000..8875e31249 --- /dev/null +++ b/embrace-gradle-plugin-integration-tests/src/test/resources/fakesettings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + mavenLocal() + } + plugins { + val agpVersion = extra["agp_version"] as String + val snapshotVersion = extra["plugin_snapshot_version"] as String + val kotlinVersion = extra["kotlin_version"] as String + + id("com.android.application").version(agpVersion) + id("org.jetbrains.kotlin.android").version(kotlinVersion) + id("io.embrace.swazzler").version(snapshotVersion) + id("io.embrace.android.testplugin").version(snapshotVersion) + } +} diff --git a/embrace-gradle-plugin/build.gradle.kts b/embrace-gradle-plugin/build.gradle.kts new file mode 100644 index 0000000000..07fb791baa --- /dev/null +++ b/embrace-gradle-plugin/build.gradle.kts @@ -0,0 +1,160 @@ +plugins { + kotlin("jvm") + id("java-gradle-plugin") + id("groovy") + id("maven-publish") + id("signing") + alias(libs.plugins.google.ksp) + alias(libs.plugins.buildconfig) + id("io.embrace.internal.build-logic") +} + +embrace { + productionModule.set(false) + androidModule.set(false) + jvmTarget.set(JavaVersion.VERSION_11) +} + +dependencies { + compileOnly(libs.agp.api) + compileOnly(gradleApi()) + + implementation(libs.okhttp) + implementation(libs.moshi) + ksp(libs.moshi.kotlin.codegen) + implementation(libs.zstd.jni) + implementation(libs.asm.util) + + testImplementation(libs.agp.api) + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.mockwebserver) + testImplementation(project(":embrace-test-common")) +} + +buildConfig { + val version: String by project + buildConfigField("String", "VERSION", "\"$version\"") +} + +allprojects { + extra["signing.keyId"] = System.getenv("mavenSigningKeyId") + extra["signing.secretKeyRingFile"] = System.getenv("mavenSigningKeyRingFile") + extra["signing.password"] = System.getenv("mavenSigningKeyPassword") +} + +// marker artifact publication +gradlePlugin { + plugins { + create("swazzlerPlugin") { + id = "io.embrace.swazzler" + group = "io.embrace" + implementationClass = "io.embrace.android.gradle.plugin.EmbraceGradlePlugin" + displayName = "Embrace Gradle Plugin" + description = "The Embrace Gradle plugin uploads mapping information and instruments bytecode" + } + } +} + +// maven-publish plugin publications settings +publishing { + publications { + create("pluginMaven") { + pom { + artifactId = "embrace-swazzler" + name = "embrace-swazzler" + group = "io.embrace" + description = "Embrace Swazzler Gradle Plugin" + url = "https://github.com/embrace-io/embrace-android-sdk" + licenses { + license { + name = "Embrace License" + url = "https://embrace.io/docs/terms-of-service/" + } + } + developers { + developer { + id = "dev1" + name = "Embrace" + email = "support@embrace.io" + } + } + scm { + connection = "scm:git:github.com/embrace-io/embrace-android-sdk.git" + developerConnection = "scm:git:ssh://github.com/embrace-io/embrace-android-sdk.git" + url = "https://github.com/embrace-io/embrace-android-sdk/tree/main" + } + } + } + } + // I need afterEvaluate otherwise it does not find swazzlerPluginPluginMarkerMaven + afterEvaluate { + publications { + named("swazzlerPluginPluginMarkerMaven") { + pom { + name = "embrace-swazzler" + artifactId = "io.embrace.swazzler.gradle.plugin" + group = "io.embrace" + description = "Embrace Gradle Plugin" + url = "https://github.com/embrace-io/embrace-android-sdk" + licenses { + license { + name = "Embrace License" + url = "https://embrace.io/docs/terms-of-service/" + } + } + developers { + developer { + id = "dev1" + name = "Embrace" + email = "support@embrace.io" + } + } + scm { + connection = "scm:git:github.com/embrace-io/embrace-android-sdk.git" + developerConnection = "scm:git:ssh://github.com/embrace-io/embrace-android-sdk.git" + url = "https://github.com/embrace-io/embrace-android-sdk/tree/main" + } + } + } + } + signing { + setRequired { gradle.taskGraph.hasTask("publishSwazzlerPluginPluginMarkerMavenPublicationToSonatypeRepository") } + sign(publishing.publications["swazzlerPluginPluginMarkerMaven"]) + } + } + + repositories { + // beta releases + maven { + credentials { + username = System.getenv("MAVEN_QA_USER") + password = System.getenv("MAVEN_QA_PASSWORD") + } + name = "Qa" + url = uri("https://repo.embrace.io/repository/beta") + } + // the android-testing maven repository is used for publishing snapshots + maven { + credentials { + username = System.getenv("MAVEN_QA_USER") + password = System.getenv("MAVEN_QA_PASSWORD") + } + name = "Snapshot" + url = uri("https://repo.embrace.io/repository/android-testing") + } + // sonatype repo that provides path to publishing on mavencentral + maven { + credentials { + username = System.getenv("SONATYPE_USERNAME") + password = System.getenv("SONATYPE_PASSWORD") + } + name = "sonatype" + url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2") + } + } + signing { + setRequired { gradle.taskGraph.hasTask("publishPluginMavenPublicationToSonatypeRepository") } + sign(publishing.publications["pluginMaven"]) + } +} diff --git a/embrace-gradle-plugin/config/detekt/baseline.xml b/embrace-gradle-plugin/config/detekt/baseline.xml new file mode 100644 index 0000000000..05308663d1 --- /dev/null +++ b/embrace-gradle-plugin/config/detekt/baseline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePlugin.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePlugin.kt new file mode 100644 index 0000000000..903c3d2c81 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePlugin.kt @@ -0,0 +1,42 @@ +package io.embrace.android.gradle.plugin + +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Plugin +import org.gradle.api.Project + +const val EXTENSION_NAME = "swazzler" +private const val ANDROID_APPLICATION_PLUGIN = "com.android.application" + +/** + * Entry point for Embrace gradle plugin. + */ +class EmbraceGradlePlugin : Plugin { + + private val impl by lazy { + EmbraceGradlePluginDelegate() + } + + override fun apply(project: Project) { + val extension = project.extensions.create( + EXTENSION_NAME, + SwazzlerExtension::class.java, + project.objects + ) + + // this property will hold configuration for each variant. It will be updated each time a new variant + // is configured + val variantConfigurationsListProperty = + project.objects.listProperty(VariantConfig::class.java).convention(emptyList()) + + // If the target project is an application, execute the task registration action that will + // add the tasks defined by the embrace gradle plugin to the build process. + project.pluginManager.withPlugin(ANDROID_APPLICATION_PLUGIN) { + impl.onAndroidPluginApplied( + project, + variantConfigurationsListProperty, + extension + ) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePluginDelegate.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePluginDelegate.kt new file mode 100644 index 0000000000..7b36780f2a --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/EmbraceGradlePluginDelegate.kt @@ -0,0 +1,139 @@ +package io.embrace.android.gradle.plugin + +import io.embrace.android.gradle.plugin.agp.AgpUtils +import io.embrace.android.gradle.plugin.agp.AgpVersion +import io.embrace.android.gradle.plugin.agp.AgpWrapper +import io.embrace.android.gradle.plugin.agp.AgpWrapperImpl +import io.embrace.android.gradle.plugin.buildreporter.BuildTelemetryService +import io.embrace.android.gradle.plugin.config.PluginBehaviorImpl +import io.embrace.android.gradle.plugin.config.variant.EmbraceVariantConfigurationBuilderForValueSource +import io.embrace.android.gradle.plugin.extension.EXTENSION_EMBRACE_INTERNAL +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.gradle.getProperty +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.instrumentation.registerAsmTasks +import io.embrace.android.gradle.plugin.network.OkHttpNetworkService +import io.embrace.android.gradle.plugin.tasks.registration.TaskRegistrar +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty + +/** + * Entry point for Android-specific code in the Embrace gradle plugin. + */ +class EmbraceGradlePluginDelegate { + + private val logger: Logger = Logger(EmbraceGradlePluginDelegate::class.java) + + fun onAndroidPluginApplied( + project: Project, + variantConfigurationsListProperty: ListProperty, + extension: SwazzlerExtension + ) { + val behavior = PluginBehaviorImpl(project, extension) + Logger.setPluginLogLevel(behavior.logLevel) + val agpExtension = AgpWrapperImpl(project) + val networkService = OkHttpNetworkService(behavior.baseUrl) + + project.extensions.create( + EXTENSION_EMBRACE_INTERNAL, + EmbraceExtensionInternal::class.java, + project.objects + ) + + // we don't want anyone to update it once it's read + variantConfigurationsListProperty.finalizeValueOnRead() + + BuildTelemetryService.register( + project, + variantConfigurationsListProperty, + behavior, + ) + + // bytecode instrumentation must be registered before project evaluation + registerAsmTasks(project, behavior) + + val embraceVariantConfigurationBuilder = + EmbraceVariantConfigurationBuilderForValueSource( + project.layout.projectDirectory, + project.providers + ) + + val taskRegistrar = TaskRegistrar( + project, + behavior, + embraceVariantConfigurationBuilder, + variantConfigurationsListProperty, + networkService, + ) + + taskRegistrar.registerTasks() + + project.afterEvaluate { evaluatedProject -> + onProjectEvaluated(evaluatedProject, agpExtension) + } + } + + /** + * It gets called once this project has been evaluated. + */ + private fun onProjectEvaluated(evaluatedProject: Project, agpWrapper: AgpWrapper) { + verifyDesugaringForOldDevices(agpWrapper) + verifySemConvWorkaround(evaluatedProject, agpWrapper) + } + + private fun verifyDesugaringForOldDevices(agpWrapper: AgpWrapper) { + var enableDesugaring = false + try { + val shouldEnableDesugaring = when (val minSdk = agpWrapper.minSdk) { + null -> true + else -> AgpUtils.isDesugaringRequired(minSdk) + } + if (shouldEnableDesugaring) { + enableDesugaring = !agpWrapper.isCoreLibraryDesugaringEnabled + } + } catch (e: Throwable) { + logger.info( + "There was an exception while checking if desugaring is required. " + + "We will consider desugaring is not required. Build will continue normally." + ) + } + + if (enableDesugaring) { + error( + "Desugaring must be enabled when minSdk is < 24, " + + "otherwise devices on Android API version < 24 will fail at runtime.\n" + + "You can enable desugaring by adding the following:\n" + + "compileOptions\n" + + "{\n" + + " coreLibraryDesugaringEnabled = true\n" + + "}\n" + + "dependencies {\n" + + " coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'\n" + + "}\n" + + "in your app\'s module build.gradle file. Use a newer version of the desugaring library if required.\n" + + "You can find more info at: \n" + + "https://developer.android.com/studio/write/java8-support#library-desugaring" + ) + } + } + + private fun verifySemConvWorkaround(project: Project, agpWrapper: AgpWrapper) { + val minSdk = agpWrapper.minSdk ?: return + + if (minSdk < 24) { + if (agpWrapper.version <= AgpVersion.AGP_8_3_0 || + project.getProperty("android.useFullClasspathForDexingTransform").orNull != "true" + ) { + error( + "To use the Embrace SDK when your minSdk is lower than 24 " + + "you must use AGP 8.3.0+ and add android.useFullClasspathForDexingTransform=true to " + + "gradle.properties.\nAlternatively you can set your minSdk to 24 or higher.\n" + + "This avoids a desugaring bug in old AGP versions that will lead to runtime crashes on old devices.\n" + + "For the full context for this workaround, please see the following issue:" + + " https://issuetracker.google.com/issues/230454566#comment18" + ) + } + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/Logger.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/Logger.kt new file mode 100644 index 0000000000..981e891489 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/Logger.kt @@ -0,0 +1,76 @@ +package io.embrace.android.gradle.plugin + +import org.gradle.api.logging.LogLevel +import org.gradle.api.logging.Logging + +/** + * General logging utility class to be used within the plugin. + */ +class Logger(clazz: Class) { + private val gradleLogger: org.gradle.api.logging.Logger = Logging.getLogger(clazz) + private val logPrefix = "[EmbraceGradlePlugin] [${clazz.simpleName}]" + + /** + * Log a message with INFO severity. + */ + fun info(msg: String, throwable: Throwable? = null) = logIfAllowed( + LogLevel.INFO, + msg, + throwable + ) + + /** + * Log a message with WARN severity. + */ + fun warn(msg: String, throwable: Throwable? = null) = logIfAllowed( + LogLevel.WARN, + msg, + throwable + ) + + /** + * Log a message with ERROR severity. + */ + fun error(msg: String, throwable: Throwable? = null) = logIfAllowed( + LogLevel.ERROR, + msg, + throwable + ) + + /** + * Logs the message only if the current Embrace log level allows it. + */ + private fun logIfAllowed( + gradleLevel: LogLevel, + msg: String, + throwable: Throwable? + ) { + if (gradleLevel.shouldLog()) { + gradleLogger.log(gradleLevel, "$logPrefix $msg", throwable) + } + } + + companion object { + private val errorLevels = setOf(LogLevel.ERROR) + private val warnLevels = errorLevels + LogLevel.WARN + private val infoLevels = warnLevels + LogLevel.INFO + + @Volatile + private var levelsToLog: Set = emptySet() + + fun getSupportedLogLevel(logLevelString: String?): LogLevel? = infoLevels.firstOrNull { + it.name.equals(logLevelString, true) + } + + fun setPluginLogLevel(logLevel: LogLevel?) { + levelsToLog = when (logLevel) { + LogLevel.INFO -> infoLevels + LogLevel.WARN -> warnLevels + LogLevel.ERROR -> errorLevels + else -> emptySet() + } + } + + private fun LogLevel.shouldLog(): Boolean = levelsToLog.contains(this) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpUtils.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpUtils.kt new file mode 100644 index 0000000000..5bbbba979c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpUtils.kt @@ -0,0 +1,16 @@ +package io.embrace.android.gradle.plugin.agp + +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider + +object AgpUtils { + + private const val MINIMUM_DESUGARING_LEVEL = 23 + fun isDesugaringRequired(sdkLevel: Int) = sdkLevel <= MINIMUM_DESUGARING_LEVEL + + /** + * It determines if given task is involved in dexguard framework. + */ + fun isDexguard(nativeObfuscationTask: TaskProvider) = + nativeObfuscationTask.name.lowercase().contains("dexguard") +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpVersion.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpVersion.kt new file mode 100644 index 0000000000..14ebb4ec88 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpVersion.kt @@ -0,0 +1,18 @@ +package io.embrace.android.gradle.plugin.agp + +import com.android.build.api.AndroidPluginVersion + +sealed class AgpVersion(private val version: AndroidPluginVersion) : Comparable { + + class CURRENT(version: AndroidPluginVersion) : AgpVersion(version) + object AGP_8_3_0 : AgpVersion(AndroidPluginVersion(8, 3, 0)) + object AGP_8_0_0 : AgpVersion(AndroidPluginVersion(8, 0, 0)) + + override fun compareTo(other: AgpVersion): Int { + return version.compareTo(other.version) + } + + override fun toString(): String { + return "${version.major}.${version.minor}.${version.micro}" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapper.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapper.kt new file mode 100644 index 0000000000..1c2c103e37 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapper.kt @@ -0,0 +1,12 @@ +package io.embrace.android.gradle.plugin.agp + +/** + * Shim around AGP's DSL. This abstracts away any changes between different AGP versions we support. + */ +interface AgpWrapper { + val isCoreLibraryDesugaringEnabled: Boolean + val usesCMake: Boolean + val usesNdkBuild: Boolean + val minSdk: Int? + val version: AgpVersion +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapperImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapperImpl.kt new file mode 100644 index 0000000000..4e1d27c5bc --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/agp/AgpWrapperImpl.kt @@ -0,0 +1,36 @@ +package io.embrace.android.gradle.plugin.agp + +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.variant.AndroidComponentsExtension +import org.gradle.api.Project + +class AgpWrapperImpl(project: Project) : AgpWrapper { + + private val extension: CommonExtension<*, *, *, *> = checkNotNull( + project.extensions.findByType(CommonExtension::class.java) + ) + + private val componentsExtension: AndroidComponentsExtension<*, *, *> = checkNotNull( + project.extensions.findByType(AndroidComponentsExtension::class.java) + ) + + override val version by lazy { + AgpVersion.CURRENT(componentsExtension.pluginVersion) + } + + override val isCoreLibraryDesugaringEnabled: Boolean by lazy { + extension.compileOptions.isCoreLibraryDesugaringEnabled + } + + override val usesCMake: Boolean by lazy { + extension.externalNativeBuild.cmake.path != null + } + + override val usesNdkBuild: Boolean by lazy { + extension.externalNativeBuild.ndkBuild.path != null + } + + override val minSdk: Int? by lazy { + extension.defaultConfig.minSdk + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildEventsListenerRegistryProvider.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildEventsListenerRegistryProvider.kt new file mode 100644 index 0000000000..3d16b3f560 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildEventsListenerRegistryProvider.kt @@ -0,0 +1,26 @@ +package io.embrace.android.gradle.plugin.buildreporter + +import org.gradle.build.event.BuildEventsListenerRegistry +import javax.inject.Inject + +/** + * It serves as a provider to fetch a BuildEventsListenerRegistry. + */ +interface BuildEventsListenerRegistryProvider { + + /** + * Gradle will automatically inject it. It is Needed to register for task events. + * Please note that in order for gradle to inject it, this class needs to be created by Gradle. + * That can be achieved by doing something like: + * ObjectFactory.newInstance(BuildEventsListenerRegistryProvider::class.java). + * + * Basically, starting on gradle 6.1, BuildService feature is supported. + * So what will end up happening is that Gradle will override this method and inject our listener registry. + * + * For gradle versions lower than 6.1, BuildService feature is not supported. So with this approach, we are + * decoupling this method and letting someone else call us. Whoever calls this method needs to + * make sure that gradle's runtime version is at least 6.1. + */ + @Inject + fun getBuildEventsListenerRegistry(): BuildEventsListenerRegistry +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryCollector.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryCollector.kt new file mode 100644 index 0000000000..2a1cfc4d8c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryCollector.kt @@ -0,0 +1,106 @@ +package io.embrace.android.gradle.plugin.buildreporter + +import io.embrace.android.gradle.plugin.agp.AgpWrapperImpl +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.gradle.GradleVersion +import io.embrace.android.gradle.plugin.gradle.GradleVersion.Companion.isAtLeast +import io.embrace.android.gradle.plugin.gradle.getProperty +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.system.SystemWrapper +import io.embrace.embrace_gradle_plugin.BuildConfig +import org.gradle.api.Project +import org.gradle.api.internal.StartParameterInternal +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import java.util.UUID + +/** + * Collects metadata about a customer's build. + */ +class BuildTelemetryCollector { + + private val systemWrapper = SystemWrapper() + + fun collect( + project: Project, + behavior: PluginBehavior, + providerFactory: ProviderFactory, + variantConfigs: ListProperty, + ): Provider { + // first, get telemetry that is ok to capture during the configuration phase + val configPhaseTelemetry = with(project) { + BuildTelemetryRequest( + agpVersion = AgpWrapperImpl(this).version.toString(), + gradleVersion = getGradleVersion(this), + isBuildCacheEnabled = isBuildCacheEnabled(), + isConfigCacheEnabled = isConfigurationCacheEnabled(), + isGradleParallelExecutionEnabled = isParallelExecutionEnabled(), + jvmArgs = getJvmArgs(), + isEdmEnabled = behavior.isUnityEdmEnabled, + edmVersion = getEdmVersion(), + metadataRequestId = UUID.randomUUID().toString(), + pluginVersion = BuildConfig.VERSION, + operatingSystem = getOperatingSystem(), + jreVersion = getJreVersion(), + jdkVersion = getJdkVersion(), + ) + } + // then return a provider that is invoked in the execution phase to avoid + // accidentally performing eager initialisations + return providerFactory.provider { + configPhaseTelemetry.copy( + variantMetadata = variantConfigs.get().map { config -> + BuildTelemetryVariant( + variantName = config.variantName, + appId = config.embraceConfig?.appId, + buildId = config.buildId, + ) + }, + ) + } + } + + private fun getGradleVersion(project: Project) = project.gradle.gradleVersion + + private fun getOperatingSystem(): String { + val osName = getSystemProperty(SYS_PROP_OS_NAME) + val osVersion = getSystemProperty(SYS_PROP_OS_VERSION) + val osArch = getSystemProperty(SYS_PROP_OS_ARCH) + return "$osName $osVersion $osArch" + } + + private fun getJreVersion() = getSystemProperty(SYS_PROP_JRE_VERSION) + private fun getJdkVersion() = getSystemProperty(SYS_PROP_JDK_VERSION) + private fun getSystemProperty(key: String) = systemWrapper.getProperty(key) ?: "" + + private fun Project.isBuildCacheEnabled() = this.gradle.startParameter.isBuildCacheEnabled + + private fun Project.isConfigurationCacheEnabled(): Boolean { + return try { + if (isAtLeast(GradleVersion.GRADLE_7_6)) { + val isConfigurationCacheRequestedMethod = + this.gradle.startParameter::class.java.getMethod("isConfigurationCacheRequested") + return isConfigurationCacheRequestedMethod.invoke(this.gradle.startParameter) as Boolean + } else { + (this.gradle.startParameter as StartParameterInternal).configurationCache.get() + } + } catch (e: Throwable) { + false + } + } + + private fun Project.isParallelExecutionEnabled() = + this.gradle.startParameter.isParallelProjectExecutionEnabled + + private fun Project.getJvmArgs() = getProperty(GRADLE_JVM_ARGS).orNull ?: "" + private fun Project.getEdmVersion() = getProperty(EMBRACE_UNITY_EDM_VERSION).orNull ?: "" +} + +private const val SYS_PROP_JRE_VERSION = "java.runtime.version" +private const val SYS_PROP_JDK_VERSION = "java.version" +private const val SYS_PROP_OS_NAME = "os.name" +private const val SYS_PROP_OS_VERSION = "os.version" +private const val SYS_PROP_OS_ARCH = "os.arch" +private const val GRADLE_JVM_ARGS = "org.gradle.jvmargs" +private const val EMBRACE_UNITY_EDM_VERSION = "embrace.edmVersion" diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryRequest.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryRequest.kt new file mode 100644 index 0000000000..75d15b2125 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryRequest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.gradle.plugin.buildreporter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class BuildTelemetryRequest( + @Json(name = "id") val metadataRequestId: String, + @Json(name = "v") val variantMetadata: List? = null, + @Json(name = "sv") val pluginVersion: String? = null, + @Json(name = "gv") val gradleVersion: String? = null, + @Json(name = "agpv") val agpVersion: String? = null, + @Json(name = "bc") val isBuildCacheEnabled: Boolean? = null, + @Json(name = "cc") val isConfigCacheEnabled: Boolean? = null, + @Json(name = "gpe") val isGradleParallelExecutionEnabled: Boolean? = null, + @Json(name = "jvma") val jvmArgs: String? = null, + @Json(name = "os") val operatingSystem: String? = null, + @Json(name = "jre") val jreVersion: String? = null, + @Json(name = "jdk") val jdkVersion: String? = null, + @Json(name = "edm") val isEdmEnabled: Boolean? = null, + @Json(name = "edmv") val edmVersion: String? = null, +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryService.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryService.kt new file mode 100644 index 0000000000..06a2ab7609 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryService.kt @@ -0,0 +1,75 @@ +package io.embrace.android.gradle.plugin.buildreporter + +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.network.OkHttpNetworkService +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.api.services.BuildServiceSpec +import org.gradle.tooling.events.FinishEvent +import org.gradle.tooling.events.OperationCompletionListener + +/** + * Initiates a HTTP request to send build telemetry data to Embrace. + */ +abstract class BuildTelemetryService : + BuildService, + AutoCloseable, + OperationCompletionListener { + + interface Params : BuildServiceParameters { + val request: Property + val baseUrl: Property + } + + /** + * It gets called moments before build is about to finish. + */ + override fun close() { + val networkService = OkHttpNetworkService(parameters.baseUrl.get()) + networkService.postBuildTelemetry(parameters.request.get()) + } + + override fun onFinish(event: FinishEvent?) { + } + + companion object { + + fun register( + project: Project, + variantConfigurations: ListProperty, + behavior: PluginBehavior + ): Provider { + if (behavior.isTelemetryDisabled) { + return project.provider { null } + } + val serviceProvider: Provider = + project.gradle.sharedServices + .registerIfAbsent( + BuildTelemetryService::class.java.name, + BuildTelemetryService::class.java + ) { serviceSpec: BuildServiceSpec -> + serviceSpec.parameters { params: BuildTelemetryService.Params -> + val telemetryProvider = BuildTelemetryCollector().collect( + project, + behavior, + project.providers, + variantConfigurations, + ) + params.request.set(telemetryProvider) + params.baseUrl.set(behavior.baseUrl) + } + } + + // subscribe for tasks events + project.objects.newInstance( + BuildEventsListenerRegistryProvider::class.java + ).getBuildEventsListenerRegistry().onTaskCompletion(serviceProvider) + return serviceProvider + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryVariant.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryVariant.kt new file mode 100644 index 0000000000..4b1a2f8efa --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/buildreporter/BuildTelemetryVariant.kt @@ -0,0 +1,18 @@ +package io.embrace.android.gradle.plugin.buildreporter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class BuildTelemetryVariant( + @Json(name = "vn") val variantName: String? = null, + @Json(name = "aid") val appId: String? = null, + @Json(name = "bid") val buildId: String? = null, +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt new file mode 100644 index 0000000000..3e51f3145f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehavior.kt @@ -0,0 +1,47 @@ +package io.embrace.android.gradle.plugin.config + +import com.android.build.api.instrumentation.InstrumentationScope + +interface InstrumentationBehavior { + + /** + * Whether the project should poison the bytecode instrumentation cache to force + * things to run again + */ + val invalidateBytecode: Boolean + + /** + * The scope of what modules get instrumented, set via `embrace.instrumentationScope` + */ + val scope: InstrumentationScope + + /** + * Whether OkHttp should be auto-instrumented + */ + val okHttpEnabled: Boolean + + /** + * Whether View clicks should be auto-instrumented + */ + val onClickEnabled: Boolean + + /** + * Whether View long clicks should be auto-instrumented + */ + val onLongClickEnabled: Boolean + + /** + * Whether WebViews should be auto-instrumented + */ + val webviewEnabled: Boolean + + /** + * Whether FCM push notifications should be auto-instrumented + */ + val fcmPushNotificationsEnabled: Boolean + + /** + * A list of string regexes that are used to filter classes during bytecode instrumentation + */ + val ignoredClasses: List +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt new file mode 100644 index 0000000000..cce587a159 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImpl.kt @@ -0,0 +1,49 @@ +package io.embrace.android.gradle.plugin.config + +import com.android.build.api.instrumentation.InstrumentationScope +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Project + +class InstrumentationBehaviorImpl( + private val project: Project, + private val extension: SwazzlerExtension, +) : InstrumentationBehavior { + + override val invalidateBytecode: Boolean by lazy { + extension.forceIncrementalOverwrite.get() + } + + override val scope: InstrumentationScope by lazy { + val prop = project.getProperty(EMBRACE_INSTRUMENTATION_SCOPE) + ?: return@lazy InstrumentationScope.ALL + try { + InstrumentationScope.valueOf(prop.uppercase()) + } catch (e: IllegalArgumentException) { + InstrumentationScope.ALL + } + } + + override val okHttpEnabled: Boolean by lazy { + extension.instrumentOkHttp.get() + } + + override val onClickEnabled: Boolean by lazy { + extension.instrumentOnClick.get() + } + + override val onLongClickEnabled: Boolean by lazy { + extension.instrumentOnLongClick.get() + } + + override val webviewEnabled: Boolean by lazy { + extension.instrumentWebview.get() + } + + override val fcmPushNotificationsEnabled: Boolean by lazy { + extension.instrumentFirebaseMessaging.get() + } + + override val ignoredClasses: List by lazy { + extension.classSkipList.get() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehavior.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehavior.kt new file mode 100644 index 0000000000..522b077c61 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehavior.kt @@ -0,0 +1,82 @@ +package io.embrace.android.gradle.plugin.config + +import org.gradle.api.logging.LogLevel + +interface PluginBehavior { + + /** + * The log level, set via `embrace.logLevel` + */ + val logLevel: LogLevel? + + /** + * Whether telemetry can be captured for this build, set via `embrace.disableCollectBuildData`. + */ + val isTelemetryDisabled: Boolean + + /** + * Whether Unity EDM is enabled, set via `embrace.externalDependencyManager` + */ + val isUnityEdmEnabled: Boolean + + /** + * Whether IL2CPP (Unity) mapping file upload is enabled, set via `embrace.uploadIl2CppMappingFiles` + */ + val isIl2CppMappingFilesUploadEnabled: Boolean + + /** + * Whether JVM mapping file upload is enabled, set via `embrace.disableMappingFileUpload` + */ + val isUploadMappingFilesDisabled: Boolean + + /** + * The base URL for the Embrace API, set via `embrace.baseUrl` + */ + val baseUrl: String + + /** + * Whether the project uses React Native or not + */ + val isReactNativeProject: Boolean + + /** + * The behavior for instrumenting bytecode + */ + val instrumentation: InstrumentationBehavior + + /** + * Whether the project should automatically add embrace dependencies to the classpath, + * set via `swazzler.disableDependencyInjection` + */ + val autoAddEmbraceDependencies: Boolean + + /** + * Whether the project should automatically add the embrace compose dependency to the classpath, + * set via `swazzler.disableComposeDependencyInjection` + */ + val autoAddEmbraceComposeDependency: Boolean + + /** + * A custom directory containing SO files, set via `swazzler.customSymbolsDirectory` + */ + val customSymbolsDirectory: String? + + /** + * Whether bytecode instrumentation is disabled for this variant, set via `swazzler.variantFilter`. + */ + fun isInstrumentationDisabledForVariant(variantName: String): Boolean + + /** + * Whether the plugin is disabled for this variant, set via `swazzler.variantFilter`. + */ + fun isPluginDisabledForVariant(variantName: String): Boolean +} + +const val EMBRACE_BASE_URL = "embrace.baseUrl" +const val EMBRACE_LOG_LEVEL = "embrace.logLevel" +const val EMBRACE_INSTRUMENTATION_SCOPE = "embrace.instrumentationScope" +const val EMBRACE_DISABLE_COLLECT_BUILD_DATA = "embrace.disableCollectBuildData" +const val EMBRACE_UPLOAD_IL2CPP_MAPPING_FILES = "embrace.uploadIl2CppMappingFiles" +const val EMBRACE_DISABLE_MAPPING_FILE_UPLOAD = "embrace.disableMappingFileUpload" +const val EMBRACE_UNITY_EXTERNAL_DEPENDENCY_MANAGER = "embrace.externalDependencyManager" +const val DEFAULT_SYMBOL_STORE_HOST_URL = "https://dsym-store.emb-api.com" diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImpl.kt new file mode 100644 index 0000000000..3ad940e5a6 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImpl.kt @@ -0,0 +1,87 @@ +package io.embrace.android.gradle.plugin.config + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Project +import org.gradle.api.logging.LogLevel +import java.io.File + +class PluginBehaviorImpl( + private val project: Project, + private val extension: SwazzlerExtension, +) : PluginBehavior { + + override val instrumentation: InstrumentationBehavior by lazy { + InstrumentationBehaviorImpl(project, extension) + } + + override val logLevel: LogLevel? by lazy { + Logger.getSupportedLogLevel(project.getProperty(EMBRACE_LOG_LEVEL)) + } + + override val isTelemetryDisabled: Boolean by lazy { + project.getBoolProperty(EMBRACE_DISABLE_COLLECT_BUILD_DATA) + } + + override val isUnityEdmEnabled: Boolean by lazy { + project.getBoolProperty(EMBRACE_UNITY_EXTERNAL_DEPENDENCY_MANAGER) + } + + override val isIl2CppMappingFilesUploadEnabled: Boolean by lazy { + project.getBoolProperty(EMBRACE_UPLOAD_IL2CPP_MAPPING_FILES) + } + + override val isUploadMappingFilesDisabled: Boolean by lazy { + project.getBoolProperty(EMBRACE_DISABLE_MAPPING_FILE_UPLOAD) + } + + override val baseUrl: String by lazy { + val prop = project.getProperty(EMBRACE_BASE_URL) + ?: return@lazy DEFAULT_SYMBOL_STORE_HOST_URL + if (prop.startsWith("http://") || prop.startsWith("https://")) { + prop + } else { + "https://$prop" + } + } + + override val isReactNativeProject: Boolean by lazy { + val rootFile = project.layout.projectDirectory.asFile.parentFile?.parentFile + if (rootFile != null) { + val nodeModules = File("${rootFile.path}/node_modules") + val nodeModulesEmbrace = File("${nodeModules.path}/react-native") + nodeModulesEmbrace.exists() + } else { + false + } + } + + override val autoAddEmbraceDependencies: Boolean by lazy { + !extension.disableDependencyInjection.get() && !isUnityEdmEnabled + } + + override val autoAddEmbraceComposeDependency: Boolean by lazy { + !extension.disableComposeDependencyInjection.get() + } + + @Suppress("DEPRECATION") + override val customSymbolsDirectory: String? by lazy { + extension.customSymbolsDirectory.get() + } + + override fun isInstrumentationDisabledForVariant(variantName: String): Boolean { + val variant = findVariant(variantName) + return !variant.enabled + } + + override fun isPluginDisabledForVariant(variantName: String): Boolean { + val variant = findVariant(variantName) + return variant.swazzlerOff + } + + private fun findVariant(variantName: String): SwazzlerExtension.Variant { + return SwazzlerExtension.Variant(variantName).also { + extension.variantFilter?.execute(it) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectExt.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectExt.kt new file mode 100644 index 0000000000..4221dc8c64 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectExt.kt @@ -0,0 +1,16 @@ +package io.embrace.android.gradle.plugin.config + +import groovy.lang.MissingPropertyException +import org.gradle.api.Project + +internal fun Project.getBoolProperty(name: String): Boolean { + return getProperty(name) == "true" +} + +internal fun Project.getProperty(name: String): String? { + return try { + property(name) as? String + } catch (exc: MissingPropertyException) { + null + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectType.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectType.kt new file mode 100644 index 0000000000..07870408db --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectType.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.plugin.config + +enum class ProjectType { + NATIVE, + REACT_NATIVE, + UNITY, + OTHER +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectTypeVerifier.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectTypeVerifier.kt new file mode 100644 index 0000000000..9eced7721a --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/ProjectTypeVerifier.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.plugin.config + +import io.embrace.android.gradle.plugin.agp.AgpWrapper +import org.gradle.api.provider.Provider + +internal object ProjectTypeVerifier { + + @JvmStatic + fun getProjectType( + unitySymbolsDir: Provider, + agpWrapper: AgpWrapper, + behavior: PluginBehavior, + ): ProjectType { + return when { + isNative(agpWrapper, behavior) -> ProjectType.NATIVE + isUnity(unitySymbolsDir.orNull) -> ProjectType.UNITY + else -> ProjectType.OTHER + } + } + + private fun isUnity(unitySymbolsDir: UnitySymbolsDir?): Boolean { + return unitySymbolsDir != null && unitySymbolsDir.isDirPresent() + } + + /** + * Check if it's a native project + */ + private fun isNative( + agpWrapper: AgpWrapper, + behavior: PluginBehavior + ): Boolean { + return agpWrapper.usesCMake || agpWrapper.usesNdkBuild || + usesCustomNativeBuild(behavior) + } + + private fun usesCustomNativeBuild(behavior: PluginBehavior): Boolean { + return !behavior.customSymbolsDirectory.isNullOrEmpty() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/UnitySymbolsDir.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/UnitySymbolsDir.kt new file mode 100644 index 0000000000..74fcdf17d9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/UnitySymbolsDir.kt @@ -0,0 +1,21 @@ +package io.embrace.android.gradle.plugin.config + +import java.io.File +import java.io.Serializable + +/** + * Holds the directory where symbols files are located. + * It also indicates if symbols are in a zip file or not. + */ +data class UnitySymbolsDir( + val unitySymbolsDir: File? = null, + val zippedSymbols: Boolean = false +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } + + fun isDirPresent() = unitySymbolsDir != null +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilder.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilder.kt new file mode 100644 index 0000000000..d87912d681 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilder.kt @@ -0,0 +1,27 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.provider.Provider + +/** + * In charge of building EmbraceVariantConfiguration. + */ +abstract class EmbraceVariantConfigurationBuilder { + + /** + * It builds variant configuration for given variant. + */ + fun buildVariantConfiguration( + variant: AndroidCompactedVariantData + ): Provider { + val variantConfiguration = buildProvider(variant) + variantConfiguration.isPresent + return variantConfiguration + } + + /** + * Subclasses will build provider of VariantConfiguration here. + */ + protected abstract fun buildProvider(variant: AndroidCompactedVariantData): Provider +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilderForValueSource.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilderForValueSource.kt new file mode 100644 index 0000000000..4a45564684 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationBuilderForValueSource.kt @@ -0,0 +1,53 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters + +/** + * It is in charge of building VariantConfiguration. + */ +class EmbraceVariantConfigurationBuilderForValueSource( + private val projectDirectory: Directory, + private val providerFactory: ProviderFactory +) : EmbraceVariantConfigurationBuilder() { + + /** + * It is in charge of fetching the config file (if available) and building VariantConfiguration from it. + */ + abstract class BuildVariantConfigurationFromFile : + ValueSource { + + interface Params : ValueSourceParameters { + // all information we need from a variant + fun getVariantInfo(): Property + + // project directory + fun getProjectDirectory(): DirectoryProperty + } + + override fun obtain(): EmbraceVariantConfig? = + EmbraceVariantConfigurationFileBuildStrategy.build( + parameters.getVariantInfo().get(), + parameters.getProjectDirectory().get() + ) + } + + /** + * It builds VariantConfiguration for given variant. + */ + override fun buildProvider(variant: AndroidCompactedVariantData): Provider { + return providerFactory.of(BuildVariantConfigurationFromFile::class.java) { + with(it.parameters) { + getVariantInfo().set(variant) + getProjectDirectory().set(projectDirectory) + } + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationFileBuildStrategy.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationFileBuildStrategy.kt new file mode 100644 index 0000000000..e4d4b0b33c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/EmbraceVariantConfigurationFileBuildStrategy.kt @@ -0,0 +1,72 @@ +package io.embrace.android.gradle.plugin.config.variant + +import com.squareup.moshi.Moshi +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import okio.buffer +import okio.source +import org.gradle.api.file.Directory +import java.io.File + +/** + * It builds an EmbraceVariantConfiguration with information fetched from a json file. + */ +object EmbraceVariantConfigurationFileBuildStrategy { + + fun build( + variantInfo: AndroidCompactedVariantData, + projectDirectory: Directory, + // all possible locations for configuration file. these are sorted by priority + // here for testing purposes + configFileFinders: List = listOf( + variantNameConfigurationFileFinder(variantInfo, projectDirectory), + variantFlavorConfigurationFileFinder(variantInfo, projectDirectory), + variantProductFlavorsConfigurationFileFinder(variantInfo, projectDirectory), + variantBuildTypeConfigurationFileFinder(variantInfo, projectDirectory), + defaultConfigurationFileFinder(projectDirectory) + ) + ): EmbraceVariantConfig? { + if (configFileFinders.isEmpty()) { + error("No config file finders found. Local configuration will not be applied.") + } + + configFileFinders.forEach { fileFinder -> + val file = fileFinder.fetchFile() + if (file != null) { + return buildVariantConfiguration(file) + } + } + + return null + } + + private fun buildVariantConfiguration( + configFile: File + ): EmbraceVariantConfig? { + configFile.inputStream().source().buffer().use { buffer -> + return try { + val moshi = Moshi.Builder().build() + val adapter = moshi.adapter(EmbraceVariantConfig::class.java) + val obj = adapter.fromJson(buffer) + val configuration = obj?.copy( + configStr = adapter.toJson(obj) + ) + + if (configuration == null) { + return null + } + + VariantConfigurationValidator.validate( + configuration = configuration, + sourceType = VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + environment = System::getenv + ) + } catch (ex: Throwable) { + throw IllegalArgumentException( + "Problem parsing field in Embrace config file " + + "${configFile.absoluteFile}.\nError=${ex.localizedMessage}" + ) + } + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationFileFinder.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationFileFinder.kt new file mode 100644 index 0000000000..0265f10ef9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationFileFinder.kt @@ -0,0 +1,66 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.file.Directory +import java.io.File + +const val VARIANT_CONFIG_FILE_NAME = "embrace-config.json" + +/** + * This class is in charge of fetching Embrace's configuration file. + */ +class VariantConfigurationFileFinder( + private val projectDirectory: Directory, + private val fileLocations: List +) { + /** + * It tries to fetch the file. + * + * @return null if file is not found + */ + fun fetchFile(): File? { + fileLocations.forEach { path -> + val file = projectDirectory.dir(path).file(VARIANT_CONFIG_FILE_NAME).asFile + if (file.exists()) { + return file + } + } + + // file was not found through this strategy + return null + } +} + +fun defaultConfigurationFileFinder(projectDirectory: Directory): VariantConfigurationFileFinder = + singleLocationFileFinder(projectDirectory, "main") + +fun variantBuildTypeConfigurationFileFinder( + variantInfo: AndroidCompactedVariantData, + projectDirectory: Directory +): VariantConfigurationFileFinder = singleLocationFileFinder(projectDirectory, variantInfo.buildTypeName) + +fun variantFlavorConfigurationFileFinder( + variantInfo: AndroidCompactedVariantData, + projectDirectory: Directory +): VariantConfigurationFileFinder = singleLocationFileFinder(projectDirectory, variantInfo.flavorName) + +fun variantNameConfigurationFileFinder( + variantInfo: AndroidCompactedVariantData, + projectDirectory: Directory +): VariantConfigurationFileFinder = singleLocationFileFinder(projectDirectory, variantInfo.name) + +fun variantProductFlavorsConfigurationFileFinder( + variantInfo: AndroidCompactedVariantData, + projectDirectory: Directory +): VariantConfigurationFileFinder = + VariantConfigurationFileFinder( + projectDirectory, + variantInfo.productFlavors.filter { + it.isNotEmpty() + }.map { + "src/$it" + }.toList() + ) + +private fun singleLocationFileFinder(projectDirectory: Directory, path: String): VariantConfigurationFileFinder = + VariantConfigurationFileFinder(projectDirectory, listOf("src/$path")) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationValidator.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationValidator.kt new file mode 100644 index 0000000000..28fe337b64 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigurationValidator.kt @@ -0,0 +1,153 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.system.Environment + +private const val EMBRACE_FILE_CONFIG_DOCS_URL = + "https://embrace.io/docs/android/integration/add-embrace-sdk/#add-the-config-file" +private const val API_TOKEN_LENGTH = 32 +private const val APP_ID_LENGTH = 5 +const val EMBRACE_API_TOKEN_ENV_KEY = "EMBRACE_API_TOKEN" + +/** + * It is in charge of performing validations on VariantConfiguration. + */ +internal object VariantConfigurationValidator { + + private val logger = Logger(VariantConfigurationValidator::class.java) + + enum class VariantConfigurationSourceType { + CONFIG_FILE, + EXTENSION + } + + /** + * It validates given VariantConfiguration. + * + * @return validated VariantConfiguration. Note that values may have been updated + */ + fun validate( + configuration: EmbraceVariantConfig, + sourceType: VariantConfigurationSourceType, + environment: Environment + ): EmbraceVariantConfig { + return validateApiToken( + validateAppId( + configuration, + sourceType + ), + sourceType, + environment + ) + } + + /** + * It validates appId for given VariantConfiguration. + * AppId is now an optional value. + * + * @return validated VariantConfiguration. Note that values may have been updated + */ + private fun validateAppId( + configuration: EmbraceVariantConfig, + sourceType: VariantConfigurationSourceType + ): EmbraceVariantConfig { + with(configuration) { + if (appId != null && appId.length != APP_ID_LENGTH) { + // app id incorrect length + val msg = when (sourceType) { + VariantConfigurationSourceType.CONFIG_FILE -> + "app_id must contain exactly $APP_ID_LENGTH " + + "characters. You can also check our documentation to get more information about the " + + "configuration file at $EMBRACE_FILE_CONFIG_DOCS_URL" + + VariantConfigurationSourceType.EXTENSION -> "appId must contain exactly $APP_ID_LENGTH characters." + } + throw IllegalArgumentException(msg) + } + } + + return configuration + } + + /** + * It validates apiToken for given VariantConfiguration. + * ApiToken is now an optional value. + * + * @return validated VariantConfiguration. Note that values may have been updated + */ + private fun validateApiToken( + configuration: EmbraceVariantConfig, + sourceType: VariantConfigurationSourceType, + environment: Environment + ): EmbraceVariantConfig { + val envApiToken = environment.getVariable(EMBRACE_API_TOKEN_ENV_KEY) + + if (configuration.apiToken.isNullOrEmpty() && envApiToken.isNullOrEmpty()) { + return configuration + } + + var validatedConfiguration = configuration + + val errorMessage = if (!configuration.apiToken.isNullOrEmpty()) { + // configuration apiToken not null + if (configuration.apiToken.length != API_TOKEN_LENGTH) { + // incorrect configuration apiToken length + when (sourceType) { + VariantConfigurationSourceType.CONFIG_FILE -> + "api_token must contain exactly $API_TOKEN_LENGTH " + + "characters. You can also check our documentation to get more information about the " + + "configuration file at $EMBRACE_FILE_CONFIG_DOCS_URL" + + VariantConfigurationSourceType.EXTENSION -> + "apiToken must contain exactly $API_TOKEN_LENGTH " + + "characters." + } + } else { + // no error + null + } + } else if (!envApiToken.isNullOrEmpty()) { + if (envApiToken.length != API_TOKEN_LENGTH) { + // incorrect configuration apiToken length + when (sourceType) { + VariantConfigurationSourceType.CONFIG_FILE -> + "api_token must contain exactly $API_TOKEN_LENGTH " + + "characters. You can also check our documentation to get more information about the " + + "configuration file at $EMBRACE_FILE_CONFIG_DOCS_URL" + + VariantConfigurationSourceType.EXTENSION -> + "apiToken must contain exactly $API_TOKEN_LENGTH " + + "characters." + } + } else { + // configuration apiToken is null but environment apiToken is not null + validatedConfiguration = configuration.copy( + appId = configuration.appId, + apiToken = envApiToken, + ndkEnabled = configuration.ndkEnabled, + sdkConfig = configuration.sdkConfig, + unityConfig = configuration.unityConfig + ) + + // no error + null + } + } else { + null // no error + } + + if (!configuration.apiToken.isNullOrEmpty() && !envApiToken.isNullOrEmpty()) { + logger.warn( + "API tokens were found in both the environment variable and the configuration file. " + + "The latter (${validatedConfiguration.apiToken}) will be used." + ) + } + + if (!errorMessage.isNullOrEmpty()) { + throw IllegalArgumentException(errorMessage) + } + + return validatedConfiguration + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/DependenciesInstaller.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/DependenciesInstaller.kt new file mode 100644 index 0000000000..f9807e2f36 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/DependenciesInstaller.kt @@ -0,0 +1,99 @@ +package io.embrace.android.gradle.plugin.dependency + +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.embrace_gradle_plugin.BuildConfig +import org.gradle.api.InvalidUserDataException +import org.gradle.api.Project +import org.gradle.api.attributes.Attribute + +const val INSTALL_EMBRACE_DEPENDENCIES_ATTRIBUTE = "io.embrace.install-dependencies" + +/** + * It installs embrace dependencies (if needed) into customer's project for given variant only. + * + * It is important to describe how this works. + * + * For embrace-android-sdk dependency, we will add it directly to their own dependencies. These are the + * dependencies that the customer declared in their project (build.gradle). This dependency will be added as if the + * customer had manually added in their build.gradle -> implementation "io.embrace:embrace-android-sdk:. + * + * It is important to note that if any customer gets an exception while executing this, accusing that dependencies were + * already resolved, then we won't be able to add our dependencies. This is not our fault, and we should tell the + * customer to look for whoever is resolving dependencies at configuration time (it could be their own code or another + * plugin) and change its behavior. + * It is not suggested, and it is considered wrong to resolve dependencies at configuration time. + */ +fun Project.installDependenciesForVariant( + variantName: String, + behavior: PluginBehavior, +) { + if (!behavior.autoAddEmbraceDependencies) { + return + } + + val targetConfigurations = listOf( + fetchConfiguration("${variantName}CompileClasspath", this), + fetchConfiguration("${variantName}RuntimeClasspath", this) + ) + + targetConfigurations.forEach { targetConfiguration -> + targetConfiguration.configure { configuration -> + try { + val installEmbraceDependenciesAttributeValue = if (behavior.isPluginDisabledForVariant(variantName)) { + // do not install dependencies + "false" + } else { + // add embrace core dependency + configuration.dependencies.addLater( + provider { + val embraceCoreSdkMetadata = EmbraceDependencyMetadata.Core(BuildConfig.VERSION) + project.dependencies.create(embraceCoreSdkMetadata.gradleShortNomenclature()) + } + ) + + // add embrace core dependency + configuration.dependencies.addLater( + provider { + val embraceOkhttpMetadata = EmbraceDependencyMetadata.OkHttp(BuildConfig.VERSION) + project.dependencies.create(embraceOkhttpMetadata.gradleShortNomenclature()) + } + ) + + // true so to tell Gradle to install Embrace dependencies (okhttp, jetpack, etc) through ComponentMetadataRule + "true" + } + + // set through this consumer configuration 's attribute if we want (or not) to install dependencies + configuration.attributes { + it.attribute( + Attribute.of( + INSTALL_EMBRACE_DEPENDENCIES_ATTRIBUTE, + String::class.java + ), + installEmbraceDependenciesAttributeValue + ) + } + } catch (e: InvalidUserDataException) { + logger.error( + "This happens because someone that gets executed before the embrace gradle plugin is resolving " + + "dependencies (either explicit or implicitly) at configuration time. We recommend to find who's " + + "doing that, and fix it. Gradle does not recommend resolving dependencies during configuration " + + "phase." + ) + throw e + } + } + } +} + +/** + * It fetches the configuration, or it creates it if it doesn't exist. + */ +private fun fetchConfiguration(configurationName: String, project: Project) = with(project) { + try { + return configurations.register(configurationName) + } catch (e: InvalidUserDataException) { + logger.debug("Configuration $configurationName already exists.") + configurations.named(configurationName) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadata.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadata.kt new file mode 100644 index 0000000000..ce24b16331 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadata.kt @@ -0,0 +1,26 @@ +package io.embrace.android.gradle.plugin.dependency + +import io.embrace.embrace_gradle_plugin.BuildConfig + +// Neither group nor name will likely ever change, so it is good to be here. +private const val EMBRACE_SDK_GROUP = "io.embrace" +private const val EMBRACE_CORE_SDK_NAME = "embrace-android-sdk" +private const val EMBRACE_OKHTTP_NAME = "embrace-android-okhttp3" + +internal sealed class EmbraceDependencyMetadata( + val group: String, + val artefact: String, + val version: String, +) { + fun gradleShortNomenclature() = "$group:$artefact:$version" + class Core(version: String = BuildConfig.VERSION) : EmbraceDependencyMetadata( + EMBRACE_SDK_GROUP, + EMBRACE_CORE_SDK_NAME, + version + ) + class OkHttp(version: String = BuildConfig.VERSION) : EmbraceDependencyMetadata( + EMBRACE_SDK_GROUP, + EMBRACE_OKHTTP_NAME, + version + ) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternal.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternal.kt new file mode 100644 index 0000000000..05e15588f0 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternal.kt @@ -0,0 +1,55 @@ +package io.embrace.android.gradle.plugin.extension + +import io.embrace.android.gradle.plugin.config.ProjectType +import io.embrace.android.gradle.plugin.config.UnitySymbolsDir +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property + +const val EXTENSION_EMBRACE_INTERNAL = "embrace-internal" + +/** + * This extension is not meant to be used by the customer. + * This is an internal extension that its purpose is to serve as a container of similar data, so it can later be + * fetched from one single place instead of multiple places. + */ +abstract class EmbraceExtensionInternal( + objectFactory: ObjectFactory +) { + val variants: NamedDomainObjectContainer = objectFactory.domainObjectContainer( + VariantExtension::class.java + ) + + abstract class VariantExtension( + objectFactory: ObjectFactory, + private val variantName: String + ) : Named { + + override fun getName() = variantName + + val apiToken: Property = objectFactory.property(String::class.java) + val ndkEnabled: Property = objectFactory.property(Boolean::class.java) + + // Include properties from VariantConfiguration + // convention(EmbraceExtensionInternal.getAppId()) + val appId: Property = objectFactory.property(String::class.java) + val config: Property = objectFactory.property(VariantConfig::class.java) + val buildId: Property = objectFactory.property(String::class.java) + + val projectType: Property = objectFactory.property(ProjectType::class.java) + val unitySymbolsDir: Property = objectFactory.property(UnitySymbolsDir::class.java) + + // we need this because older gradle can not inject ObjectFactory + fun initialize() { + appId.finalizeValueOnRead() + config.finalizeValueOnRead() + buildId.finalizeValueOnRead() + apiToken.finalizeValueOnRead() + ndkEnabled.finalizeValueOnRead() + projectType.finalizeValueOnRead() + unitySymbolsDir.finalizeValueOnRead() + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternalSource.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternalSource.kt new file mode 100644 index 0000000000..59d94dfba3 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/EmbraceExtensionInternalSource.kt @@ -0,0 +1,74 @@ +package io.embrace.android.gradle.plugin.extension + +import io.embrace.android.gradle.plugin.agp.AgpWrapper +import io.embrace.android.gradle.plugin.agp.AgpWrapperImpl +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.config.variant.EmbraceVariantConfigurationBuilder +import io.embrace.android.gradle.plugin.extension.utils.VariantConfigurationToEmbraceExtensionInternal +import io.embrace.android.gradle.plugin.gradle.GradleCompatibilityHelper +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider + +class EmbraceExtensionInternalSource { + + fun setupExtension( + project: Project, + behavior: PluginBehavior, + variant: AndroidCompactedVariantData, + embraceVariantConfigurationBuilder: EmbraceVariantConfigurationBuilder, + variantConfigurationsListProperty: ListProperty, + ) { + val embraceVariantConfiguration = + embraceVariantConfigurationBuilder.buildVariantConfiguration(variant) + val fullVariantConfiguration = + embraceVariantConfiguration.map { + VariantConfig.from( + it, + variant + ) + }.orElse( + VariantConfig.from( + null, + variant + ) + ) + + configureEmbraceExtensionInternalForVariant( + variant, + fullVariantConfiguration, + AgpWrapperImpl(project), + project, + behavior, + ) + + // let's add configuration for current variant to our property + GradleCompatibilityHelper.add(variantConfigurationsListProperty, fullVariantConfiguration) + } + + /** + * It configures EmbraceExtensionInternal for given variant. + */ + private fun configureEmbraceExtensionInternalForVariant( + variantInfo: AndroidCompactedVariantData, + variantConfigProvider: Provider, + agpWrapper: AgpWrapper, + project: Project, + behavior: PluginBehavior, + ) { + with(project.extensions) { + configure( + EmbraceExtensionInternal::class.java, + VariantConfigurationToEmbraceExtensionInternal( + variantInfo, + variantConfigProvider, + agpWrapper, + behavior, + project + ) + ) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/BaseVariantToEmbraceExtensionInternal.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/BaseVariantToEmbraceExtensionInternal.kt new file mode 100644 index 0000000000..f89459f312 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/BaseVariantToEmbraceExtensionInternal.kt @@ -0,0 +1,33 @@ +package io.embrace.android.gradle.plugin.extension.utils + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import org.gradle.api.Action +import org.gradle.api.UnknownDomainObjectException + +/** + * Base class that will create the variants extension in case it wasn't created already. + */ +abstract class BaseVariantToEmbraceExtensionInternal( + private val variantName: String +) : Action { + + private val logger = Logger(BaseVariantToEmbraceExtensionInternal::class.java) + + final override fun execute(extension: EmbraceExtensionInternal) { + try { + extension.variants.named(variantName) + } catch (e: UnknownDomainObjectException) { + extension.variants.create(variantName).also { + it.initialize() + logger.info("Created variant extension for variant=$variantName that did not previously exist") + } + } + setupVariant(extension) + } + + /** + * Hook method to set up the extension given a variant. + */ + abstract fun setupVariant(extension: EmbraceExtensionInternal) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/VariantConfigurationToEmbraceExtensionInternal.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/VariantConfigurationToEmbraceExtensionInternal.kt new file mode 100644 index 0000000000..c6531d1bb7 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/extension/utils/VariantConfigurationToEmbraceExtensionInternal.kt @@ -0,0 +1,85 @@ +package io.embrace.android.gradle.plugin.extension.utils + +import io.embrace.android.gradle.plugin.agp.AgpWrapper +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.config.ProjectTypeVerifier +import io.embrace.android.gradle.plugin.config.UnitySymbolsDir +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.tasks.il2cpp.UnitySymbolFilesManager +import org.gradle.api.Project +import org.gradle.api.provider.Provider + +private const val NDK_ENABLED_DEFAULT: Boolean = true + +/** + * It configures this extension with given VariantConfiguration properties. + */ +class VariantConfigurationToEmbraceExtensionInternal( + private val variantInfo: AndroidCompactedVariantData, + private val variantConfigProvider: Provider, + private val agpWrapper: AgpWrapper, + private val behavior: PluginBehavior, + private val project: Project +) : BaseVariantToEmbraceExtensionInternal(variantInfo.name) { + + override fun setupVariant(extension: EmbraceExtensionInternal) { + extension.variants.named(variantInfo.name).configure { + // properties from variant configuration + it.apiToken.set( + variantConfigProvider.map { variantConfig -> + variantConfig.embraceConfig?.apiToken ?: "" + } + ) + it.ndkEnabled.set( + variantConfigProvider.map { variantConfig -> + variantConfig.embraceConfig?.ndkEnabled ?: NDK_ENABLED_DEFAULT + } + ) + it.appId.set( + variantConfigProvider.map { variantConfig -> + variantConfig.embraceConfig?.appId ?: "" + } + ) + it.config.set(variantConfigProvider) + it.buildId.set( + variantConfigProvider.map { variantConfig -> + variantConfig.buildId ?: "" + } + ) + + val symbolsDir = getSymbolsDir() + val projectType = getProjectType(symbolsDir) + + it.unitySymbolsDir.set(symbolsDir) + it.projectType.set(projectType) + } + } + + // there is no need to let Gradle know about the knowledge of how to get unitySymbolsDir, because + // all properties that getSymbolsDir depends on are already config-cache aware. Meaning that if any + // of the properties that this function uses changes, Gradle will invalidate config cache. + private fun getSymbolsDir(): Provider = project.provider { + val unityConfig = if (variantConfigProvider.isPresent) { + variantConfigProvider.get().embraceConfig?.unityConfig + } else { + null + } + + val realProject = project.parent ?: project + UnitySymbolFilesManager.of().getSymbolsDir( + realProject.layout.projectDirectory, + project.layout.projectDirectory, + unityConfig + ) + } + + private fun getProjectType(symbolsDir: Provider) = project.provider { + ProjectTypeVerifier.getProjectType( + symbolsDir, + agpWrapper, + behavior, + ) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleCompatibilityHelper.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleCompatibilityHelper.kt new file mode 100644 index 0000000000..29a15cf621 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleCompatibilityHelper.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.gradle + +import io.embrace.android.gradle.plugin.gradle.GradleVersion.Companion.isAtLeast +import org.gradle.api.provider.HasMultipleValues +import org.gradle.api.provider.Provider +import java.util.Collections + +object GradleCompatibilityHelper { + + /** + * Utility method that adds a provider to a multiple values property. + * This is needed specifically because of a Gradle issue. + */ + fun add(self: HasMultipleValues, provider: Provider) { + if (isGradleAffectedByIssue22331()) { + self.addAll( + provider.map { + Collections.singletonList(it) + } + ) + } else { + self.add(provider) + } + } + + // this issue is still active, so we do not know when this will get fixed. Update this method when Gradle fix + // is in place + // issue link: https://github.com/gradle/gradle/issues/22331 + private fun isGradleAffectedByIssue22331() = !isAtLeast(GradleVersion.GRADLE_8_0) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleVersion.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleVersion.kt new file mode 100644 index 0000000000..2befb06570 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/GradleVersion.kt @@ -0,0 +1,28 @@ +package io.embrace.android.gradle.plugin.gradle + +import org.gradle.util.GradleVersion.version + +sealed class GradleVersion(private val version: org.gradle.util.GradleVersion) : + Comparable { + + object CURRENT : GradleVersion(org.gradle.util.GradleVersion.current()) + object GRADLE_7_6 : GradleVersion(version("7.6")) + object GRADLE_8_0 : GradleVersion(version("8.0")) + + override fun compareTo(other: GradleVersion): Int { + return version.compareTo(other.version) + } + + override fun toString(): String { + return version.version + } + + companion object { + + /** + * Returns true if the current AGP version exceeds the specified version. + */ + @JvmStatic + fun isAtLeast(version: GradleVersion): Boolean = CURRENT >= version + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeMap.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeMap.kt new file mode 100644 index 0000000000..73873182b6 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeMap.kt @@ -0,0 +1,32 @@ +package io.embrace.android.gradle.plugin.gradle + +import org.gradle.api.Transformer +import org.gradle.api.provider.Provider + +/** + * Workaround that uses Java's @Nullable annotation to correct some incorrect nullability + * annotations in Gradle. + *

+ * See https://github.com/gradle/gradle/issues/12388 + */ +fun Provider.nullSafeMap(transform: (I) -> O?): Provider { + return map( + object : io.embrace.android.gradle.plugin.gradle.NullSafeTransformer() { + override fun transform(input: I): O? { + return transform(input) + } + } + ) +} + +/** + * Workaround that creates an explicit anon inner class so that configuration cache isn't broken. + */ +@Suppress("ObjectLiteralToLambda") +inline fun Provider.safeFlatMap( + crossinline transform: (T) -> Provider +): Provider = flatMap(object : Transformer, T> { + override fun transform(input: T): Provider { + return transform(input) + } +}) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeTransformer.java b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeTransformer.java new file mode 100644 index 0000000000..a931f8c3cb --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/NullSafeTransformer.java @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.gradle; + +import org.gradle.api.Transformer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Workaround that uses Java's @Nullable annotation to correct some incorrect nullability + * annotations in Gradle. + *

+ * See https://github.com/gradle/gradle/issues/12388 + */ +public abstract class NullSafeTransformer implements Transformer { + @Override + @Nullable + public abstract O transform(@NotNull I input); +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/ProjectExt.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/ProjectExt.kt new file mode 100644 index 0000000000..d5efe8269d --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/ProjectExt.kt @@ -0,0 +1,7 @@ +package io.embrace.android.gradle.plugin.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Provider + +fun Project.getProperty(propertyName: String): Provider = + providers.gradleProperty(propertyName) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegister.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegister.kt new file mode 100644 index 0000000000..8a987cbe12 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegister.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.gradle + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.tasks.EmbraceTask +import io.embrace.android.gradle.plugin.util.capitalizedString +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.tasks.TaskProvider + +/** + * Registers an embrace task on the project and sets initial values on task properties. + */ +fun Project.registerTask( + name: String, + clz: Class, + variantData: AndroidCompactedVariantData, + configurationAction: Action +): TaskProvider { + val taskName = "$name${variantData.name.capitalizedString()}" + logger.info("Registering task=$taskName") + val taskProvider: TaskProvider = tasks.register(taskName, clz) { task: T -> + // at this point, it means that the task has been realized, which is ok, it means that the task is being + // configured (not necessarily ran) + logger.info("Task=$taskName has been realized.") + task.variantData.set(variantData) + configurationAction.execute(task) + logger.debug("Task=$taskName configured.") + } + return taskProvider +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegistrationUtils.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegistrationUtils.kt new file mode 100644 index 0000000000..7c086ec989 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/gradle/TaskRegistrationUtils.kt @@ -0,0 +1,46 @@ +package io.embrace.android.gradle.plugin.gradle + +import io.embrace.android.gradle.plugin.util.capitalizedString +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownDomainObjectException +import org.gradle.api.UnknownTaskException +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider + +/** + * @return if the task is already registered in the gradle task graph. + */ +fun Project.isTaskRegistered(taskName: String, variantName: String): Boolean { + return tasks.names.contains("$taskName${variantName.capitalizedString()}") +} + +fun TaskContainer.isTaskRegistered(taskName: String, variantName: String): Boolean { + return names.contains("$taskName${variantName.capitalizedString()}") +} + +/** + * It determines if given taskProvider is registered. + * + * This method could be removed, it is here for better comprehension. + */ +fun isTaskRegistered(taskProvider: TaskProvider?) = taskProvider != null + +/** + * It returns task provider for given taskName without realizing the task. + */ +fun Project.tryGetTaskProvider(taskName: String): TaskProvider? { + logger.debug("Will try to get TaskProvider for taskName=$taskName") + return try { + val task = tasks.named(taskName) + logger.debug("TaskProvider obtained with name=${task.name}") + + task + } catch (e: UnknownDomainObjectException) { + logger.debug("Task provider not found") + null + } catch (e: UnknownTaskException) { + logger.debug("Task provider not found") + null + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/hash/HashExt.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/hash/HashExt.kt new file mode 100644 index 0000000000..429e3ef655 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/hash/HashExt.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.hash + +import java.io.File +import java.security.MessageDigest +import java.util.Locale + +/** + * Calculates a SHA-1 from a file. + */ +fun calculateSha1ForFile(file: File): String { + val bytes = MessageDigest + .getInstance("SHA-1") + .digest(file.readBytes()) + return bytes.joinToString("") { "%02x".format(it) } +} + +/** + * Calculates a MD5 from a file. + */ +fun calculateMD5ForFile(file: File): String { + val hasFile: String + val md = MessageDigest.getInstance("MD5") + val fileHashed = md.digest(file.readBytes()) + val sb = StringBuilder() + for (b in fileHashed) { + sb.append(String.format(Locale.getDefault(), "%02x", b.toInt() and 0xff)) + } + hasFile = sb.toString().uppercase(Locale.getDefault()) + return hasFile +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTasks.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTasks.kt new file mode 100644 index 0000000000..9759c5c195 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/AsmTasks.kt @@ -0,0 +1,94 @@ +@file:JvmName("AsmTasks") + +package io.embrace.android.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.FramesComputationMode +import com.android.build.api.variant.AndroidComponentsExtension +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import org.gradle.api.Project +import org.gradle.api.UnknownDomainObjectException + +/** + * Registers an ASM class visitor for all build variants, which ensures that the + * relevant classes are instrumented. + */ +fun registerAsmTasks( + project: Project, + behavior: PluginBehavior +) { + // register for asm + project.extensions.getByType(AndroidComponentsExtension::class.java).onVariants { variant -> + val scope = behavior.instrumentation.scope + project.logger.info("Registered ASM task for ${variant.name} with scope=${scope.name}") + + // compute frames automatically only for modified methods + variant.instrumentation.setAsmFramesComputationMode( + FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS + ) + try { + // register the ASM class visitor factory + variant.instrumentation.transformClassesWith( + EmbraceClassVisitorFactory::class.java, + scope + ) { params: BytecodeInstrumentationParams -> + project.logger.debug("Configuring ASM instrumentation") + val embraceExtensionInternal = checkNotNull( + project.extensions.findByType(EmbraceExtensionInternal::class.java) + ) + + params.config.set( + project.provider { + val variantExtension = try { + embraceExtensionInternal.variants.getByName(variant.name) + } catch (exc: UnknownDomainObjectException) { + project.logger.lifecycle( + "Variant ${variant.name} not found in EmbraceExtensionInternal." + + "Following variants are available: ${ + embraceExtensionInternal.variants.asMap.values.joinToString( + "," + ) + }" + ) + throw exc + } + + variantExtension.config.get() + } + ) + params.logLevel.set( + project.provider { + behavior.logLevel + } + ) + params.disabled.set( + project.provider { + behavior.isPluginDisabledForVariant(variant.name) || + behavior.isInstrumentationDisabledForVariant(variant.name) + } + ) + params.classInstrumentationFilter.set( + ClassInstrumentationFilter(behavior.instrumentation.ignoredClasses) + ) + params.invalidate.set( + when { + behavior.instrumentation.invalidateBytecode -> System.currentTimeMillis() + else -> -1L // use a predictable input each time + } + ) + params.shouldInstrumentFirebaseMessaging.set(behavior.instrumentation.fcmPushNotificationsEnabled) + params.shouldInstrumentWebview.set(behavior.instrumentation.webviewEnabled) + params.shouldInstrumentOkHttp.set(behavior.instrumentation.okHttpEnabled) + params.shouldInstrumentOnLongClick.set(behavior.instrumentation.onLongClickEnabled) + params.shouldInstrumentOnClick.set(behavior.instrumentation.onClickEnabled) + } + project.logger.debug("Asm transformClassesWith successfully called.") + } catch (e: TransformClassesWithReflectionException) { + project.logger.warn( + "There was a reflection issue while performing ASM bytecode transformation.\nThis " + + "shouldn't affect build output.", + e + ) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt new file mode 100644 index 0000000000..fe241e4a51 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/BytecodeInstrumentationParams.kt @@ -0,0 +1,71 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.InstrumentationParameters +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional + +/** + * Contains parameters that affect how bytecode is manipulated during instrumentation. + * + * As each property is an [Input] the task parameters will be cached if the values do not + * change between builds. + */ +interface BytecodeInstrumentationParams : InstrumentationParameters { + + /** + * Representation of the config supplied by the user that will both alter how the plugin behaves + * & instruments the SDK. + */ + @get:Input + val config: Property + + /** + * Whether or not the plugin should operate in debug mode. This will mainly affect + * logging. + */ + @get:Optional + @get:Input + val logLevel: Property + + /** + * Whether or not the plugin should operate for this variant. + */ + @get:Input + val disabled: Property + + /** + * Acts as a user-configurable filter by discarding the classes which should be + * skipped for this variant. + */ + @get:Input + val classInstrumentationFilter: Property + + /** + * Allows for invalidating the cache if the user wants to force the Transform to run from + * scratch. + * + * This may be useful if the bytecode params have not changed but the library dependencies have. + * Gradle's default behaviour is that these changed library dependencies will not be + * instrumented unless we alter at least one parameter value here. + */ + @get:Input + val invalidate: Property + + @get:Input + val shouldInstrumentFirebaseMessaging: Property + + @get:Input + val shouldInstrumentWebview: Property + + @get:Input + val shouldInstrumentOkHttp: Property + + @get:Input + val shouldInstrumentOnLongClick: Property + + @get:Input + val shouldInstrumentOnClick: Property +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/ClassInstrumentationFilter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/ClassInstrumentationFilter.kt new file mode 100644 index 0000000000..dd5302bc1b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/ClassInstrumentationFilter.kt @@ -0,0 +1,27 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import java.io.Serializable +import java.util.regex.Pattern + +/** + * A filter that determines whether a class should be skipped, according to user-defined rules. + */ +class ClassInstrumentationFilter( + internal val skipList: List +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } + + private val skipPatterns by lazy { skipList.map(Pattern::compile) } + + /** + * @param name the class name + * @return if the given name should be skipped or not. + */ + fun shouldSkip(name: String): Boolean = skipPatterns.any { pattern -> + pattern.matcher(name).find() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactory.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactory.kt new file mode 100644 index 0000000000..9821619c2c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactory.kt @@ -0,0 +1,50 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.AsmClassVisitorFactory +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData +import io.embrace.android.gradle.plugin.Logger +import org.objectweb.asm.ClassVisitor + +/** + * A factory which creates ClassVisitor objects for instrumenting classes. This effectively + * determines which classes should go through the instrumentation process, and returns the necessary + * object to instrument the bytecode. + * + * This class should not contain any fields/direct state. Everything needs to be passed through + * [BytecodeInstrumentationParams] and marked as an input as this is used to support up-to-date checks. + */ +abstract class EmbraceClassVisitorFactory : AsmClassVisitorFactory { + + override fun createClassVisitor( + classContext: ClassContext, + nextClassVisitor: ClassVisitor + ): ClassVisitor { + return createClassVisitorImpl( + classContext, + nextClassVisitor, + instrumentationContext, + parameters + ) { + logger.info(it()) + } + } + + override fun isInstrumentable(classData: ClassData): Boolean { + val params = parameters.get() + + if (params.disabled.get()) { + return false + } + if (params.classInstrumentationFilter.get().shouldSkip(classData.className)) { + return false + } + + // any class could implement OnClickListener, so we need to search everything. + // in future we could confine this search to Activity/Fragment/View implementations at the + // cost of Swazzling 100% of onClick implementations. + return true + } +} + +private val logger = Logger(EmbraceClassVisitorFactory::class.java) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt new file mode 100644 index 0000000000..d8ed079c7d --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryDelegate.kt @@ -0,0 +1,57 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.InstrumentationContext +import io.embrace.android.gradle.plugin.instrumentation.config.ConfigClassVisitorFactory +import io.embrace.android.gradle.plugin.instrumentation.visitor.FirebaseMessagingServiceClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnLongClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.WebViewClientClassAdapter +import org.gradle.api.provider.Property +import org.objectweb.asm.ClassVisitor + +internal fun createClassVisitorImpl( + classContext: ClassContext, + nextClassVisitor: ClassVisitor, + instrumentationContext: InstrumentationContext, + parameters: Property, + logger: (() -> String) -> Unit +): ClassVisitor { + val api = instrumentationContext.apiVersion.get() + var visitor = nextClassVisitor + val className = classContext.currentClassData.className + + // Add a visitor if this is a config class provided by the SDK + val cfg = parameters.get().config.get() + ConfigClassVisitorFactory.createClassVisitor(className, cfg, api, visitor)?.let { + visitor = it + } + + // chain our own visitors to avoid unlikely (but possible) cases such as a custom + // WebViewClient implementing an OnClickListener + if (parameters.get().shouldInstrumentFirebaseMessaging.get() && + FirebaseMessagingServiceClassAdapter.accept(classContext) + ) { + visitor = FirebaseMessagingServiceClassAdapter(api, visitor, logger) + logger { "Added FirebaseMessagingServiceClassAdapter for $className." } + } + + if (parameters.get().shouldInstrumentWebview.get() && WebViewClientClassAdapter.accept(classContext)) { + visitor = WebViewClientClassAdapter(api, visitor, logger) + logger { "Added WebViewClientClassAdapter for $className." } + } + if (parameters.get().shouldInstrumentOkHttp.get() && OkHttpClassAdapter.accept(classContext)) { + visitor = OkHttpClassAdapter(api, visitor, logger) + logger { "Added OkHttpClassAdapter for $className." } + } + if (parameters.get().shouldInstrumentOnLongClick.get() && OnLongClickClassAdapter.accept(classContext)) { + visitor = OnLongClickClassAdapter(api, visitor, logger) + logger { "Added OnLongClickClassAdapter for $className." } + } + if (parameters.get().shouldInstrumentOnClick.get() && OnClickClassAdapter.accept(classContext)) { + visitor = OnClickClassAdapter(api, visitor, logger) + logger { "Added OnClickClassAdapter for $className." } + } + return visitor +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/TransformClassesWithReflectionException.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/TransformClassesWithReflectionException.kt new file mode 100644 index 0000000000..516b2db1e3 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/TransformClassesWithReflectionException.kt @@ -0,0 +1,3 @@ +package io.embrace.android.gradle.plugin.instrumentation + +class TransformClassesWithReflectionException(message: String?) : Exception(message) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/BooleanReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/BooleanReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..36db22fd55 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/BooleanReturnValueMethodVisitor.kt @@ -0,0 +1,25 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a boolean method and replaces its return value with the [replacedValue] parameter. + */ +class BooleanReturnValueMethodVisitor( + val replacedValue: Boolean, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.IRETURN) { + if (replacedValue) { + visitInsn(Opcodes.ICONST_1) + } else { + visitInsn(Opcodes.ICONST_0) + } + } + super.visitInsn(opcode) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/ConfigClassVisitorFactory.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/ConfigClassVisitorFactory.kt new file mode 100644 index 0000000000..d5c4fb6061 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/ConfigClassVisitorFactory.kt @@ -0,0 +1,54 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createBaseUrlConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createEnabledFeatureConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createNetworkCaptureConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createProjectConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createRedactionConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk.createSessionConfigInstrumentation +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.visitor.ConfigInstrumentationClassVisitor +import org.objectweb.asm.ClassVisitor + +object ConfigClassVisitorFactory { + private enum class ConfigClassType { + BaseUrlConfig, + EnabledFeatureConfig, + NetworkCaptureConfig, + ProjectConfig, + RedactionConfig, + SessionConfig; + + val className = "io.embrace.android.embracesdk.internal.config.instrumented.${name}Impl" + + fun createClassVisitor( + cfg: VariantConfig, + api: Int, + cv: ClassVisitor? + ): ClassVisitor { + val instrumentation = when (this) { + BaseUrlConfig -> createBaseUrlConfigInstrumentation(cfg) + EnabledFeatureConfig -> createEnabledFeatureConfigInstrumentation(cfg) + NetworkCaptureConfig -> createNetworkCaptureConfigInstrumentation(cfg) + ProjectConfig -> createProjectConfigInstrumentation(cfg) + RedactionConfig -> createRedactionConfigInstrumentation(cfg) + SessionConfig -> createSessionConfigInstrumentation(cfg) + } + return ConfigInstrumentationClassVisitor(instrumentation, api, cv) + } + } + + /** + * Creates a class visitor that instruments a config class in the SDK, or returns null if the + * class is not a config class. + */ + fun createClassVisitor( + className: String, + cfg: VariantConfig, + api: Int, + cv: ClassVisitor? + ): ClassVisitor? { + val type = ConfigClassType.values().singleOrNull { it.className == className } + return type?.createClassVisitor(cfg, api, cv) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/IntReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/IntReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..d98682c4aa --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/IntReturnValueMethodVisitor.kt @@ -0,0 +1,21 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a int method and replaces its return value with the [replacedValue] parameter. + */ +class IntReturnValueMethodVisitor( + val replacedValue: Int, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.IRETURN) { + visitLdcInsn(replacedValue) + } + super.visitInsn(opcode) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/LongReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/LongReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..d0ba816b3c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/LongReturnValueMethodVisitor.kt @@ -0,0 +1,21 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a long method and replaces its return value with the [replacedValue] parameter. + */ +class LongReturnValueMethodVisitor( + val replacedValue: Long, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.LRETURN) { + visitLdcInsn(replacedValue) + } + super.visitInsn(opcode) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/MapReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/MapReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..8a5e564170 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/MapReturnValueMethodVisitor.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a Map method and replaces its return value with the [replacedValue] parameter. + */ +class MapReturnValueMethodVisitor( + val replacedValue: Map, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitCode() { + super.visitCode() + + // instantiate a new map + visitTypeInsn(Opcodes.NEW, "java/util/HashMap") + visitInsn(Opcodes.DUP) + visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "", "()V", false) + + // iterate replacedValue and add each object to the list + replacedValue.forEach { entry -> + visitInsn(Opcodes.DUP) + visitLdcInsn(entry.key) + visitLdcInsn(entry.value) + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/util/HashMap", + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + false + ) + visitInsn(Opcodes.POP) // pop return value off stack + } + visitInsn(Opcodes.ARETURN) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringListReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringListReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..05eb50a983 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringListReturnValueMethodVisitor.kt @@ -0,0 +1,38 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a string method and replaces its return value with the [replacedValue] parameter. + */ +class StringListReturnValueMethodVisitor( + val replacedValue: List, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitCode() { + super.visitCode() + + // instantiate a new array list + visitTypeInsn(Opcodes.NEW, "java/util/ArrayList") + visitInsn(Opcodes.DUP) + visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/ArrayList", "", "()V", false) + + // iterate replacedValue and add each object to the list + replacedValue.forEach { value -> + visitInsn(Opcodes.DUP) + visitLdcInsn(value) + visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "java/util/ArrayList", + "add", + "(Ljava/lang/Object;)Z", + false + ) + visitInsn(Opcodes.POP) // pop return value off stack + } + visitInsn(Opcodes.ARETURN) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringReturnValueMethodVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringReturnValueMethodVisitor.kt new file mode 100644 index 0000000000..902a603273 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/StringReturnValueMethodVisitor.kt @@ -0,0 +1,21 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a string method and replaces its return value with the [replacedValue] parameter. + */ +class StringReturnValueMethodVisitor( + val replacedValue: String, + api: Int, + nextVisitor: MethodVisitor +) : MethodVisitor(api, nextVisitor) { + + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.ARETURN) { + visitLdcInsn(replacedValue) + } + super.visitInsn(opcode) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ConfigModelDsl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ConfigModelDsl.kt new file mode 100644 index 0000000000..0ab571066b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ConfigModelDsl.kt @@ -0,0 +1,50 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch + +/** + * DSL for creating a model of how an SDK config class should be instrumented. + */ +fun modelSdkConfigClass(init: InstrumentedConfigClass.() -> Unit): InstrumentedConfigClass { + return InstrumentedConfigClass().apply(init) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.boolMethod(name: String, valueProvider: () -> Boolean?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.BOOLEAN, valueProvider)) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.intMethod(name: String, valueProvider: () -> Int?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.INT, valueProvider)) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.longMethod(name: String, valueProvider: () -> Long?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.LONG, valueProvider)) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.stringMethod(name: String, valueProvider: () -> String?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.STRING, valueProvider)) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.stringListMethod(name: String, valueProvider: () -> List?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.STRING_LIST, valueProvider)) +} + +/** + * DSL to declare how an SDK config method should be instrumented. + */ +fun InstrumentedConfigClass.mapMethod(name: String, valueProvider: () -> Map?) { + addMethod(InstrumentedConfigMethod(name, ReturnType.MAP, valueProvider)) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClass.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClass.kt new file mode 100644 index 0000000000..7df47e9b1c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClass.kt @@ -0,0 +1,31 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch + +import org.objectweb.asm.MethodVisitor + +/** + * Holds data on how a SDK config class should be instrumented with configuration values. + */ +class InstrumentedConfigClass( + private val methods: MutableList = mutableListOf() +) { + + fun addMethod(method: InstrumentedConfigMethod) { + methods.add(method) + } + + /** + * Returns a MethodVisitor that will instrument the method if it is a target for configuration, + * otherwise it returns the existing visitor. + */ + fun getMethodVisitor( + name: String, + descriptor: String, + api: Int, + visitor: MethodVisitor + ): MethodVisitor { + val match = methods.singleOrNull { + it.isConfigInstrumentationTarget(name, descriptor) + } ?: return visitor + return match.getMethodVisitor(api, visitor) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigMethod.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigMethod.kt new file mode 100644 index 0000000000..0610214937 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigMethod.kt @@ -0,0 +1,60 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch + +import io.embrace.android.gradle.plugin.instrumentation.config.BooleanReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.IntReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.LongReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.MapReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringReturnValueMethodVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Holds data on how a SDK config method should be instrumented with a configuration value. + */ +class InstrumentedConfigMethod( + private val functionName: String, + private val returnType: ReturnType, + private val valueProvider: () -> Any? +) { + + fun isConfigInstrumentationTarget(name: String, descriptor: String): Boolean { + return functionName == name && descriptor == returnType.descriptor + } + + @Suppress("UNCHECKED_CAST") + fun getMethodVisitor(api: Int, visitor: MethodVisitor): MethodVisitor { + val result = valueProvider() ?: return visitor + return when (returnType) { + ReturnType.BOOLEAN -> BooleanReturnValueMethodVisitor( + result as Boolean, + api, + visitor + ) + ReturnType.INT -> IntReturnValueMethodVisitor( + result as Int, + api, + visitor + ) + ReturnType.LONG -> LongReturnValueMethodVisitor( + result as Long, + api, + visitor + ) + ReturnType.STRING -> StringReturnValueMethodVisitor( + result as String, + api, + visitor + ) + ReturnType.STRING_LIST -> io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor( + result as List, + api, + visitor + ) + ReturnType.MAP -> MapReturnValueMethodVisitor( + result as Map, + api, + visitor + ) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ReturnType.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ReturnType.kt new file mode 100644 index 0000000000..193091fbc4 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/ReturnType.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch + +/** + * The return type of a config method. + */ +enum class ReturnType(val descriptor: String) { + BOOLEAN("()Z"), + INT("()I"), + LONG("()J"), + STRING("()Ljava/lang/String;"), + STRING_LIST("()Ljava/util/List;"), + MAP("()Ljava/util/Map;"), +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentation.kt new file mode 100644 index 0000000000..0484d8eb06 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentation.kt @@ -0,0 +1,10 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringMethod +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createBaseUrlConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + stringMethod("getConfig") { cfg.embraceConfig?.sdkConfig?.baseUrls?.config } + stringMethod("getData") { cfg.embraceConfig?.sdkConfig?.baseUrls?.data } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentation.kt new file mode 100644 index 0000000000..8d2c19b88f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentation.kt @@ -0,0 +1,41 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.boolMethod +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createEnabledFeatureConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + boolMethod("isNativeCrashCaptureEnabled") { cfg.embraceConfig?.ndkEnabled } + with(cfg.embraceConfig?.sdkConfig ?: return@modelSdkConfigClass) { + boolMethod("isUnityAnrCaptureEnabled") { anr?.captureUnityThread } + boolMethod("isActivityBreadcrumbCaptureEnabled") { viewConfig?.enableAutomaticActivityCapture } + boolMethod("isComposeClickCaptureEnabled") { composeConfig?.captureComposeOnClick } + boolMethod("isViewClickCoordinateCaptureEnabled") { taps?.captureCoordinates } + boolMethod("isMemoryWarningCaptureEnabled") { automaticDataCaptureConfig?.memoryServiceEnabled } + boolMethod("isPowerSaveModeCaptureEnabled") { automaticDataCaptureConfig?.powerSaveModeServiceEnabled } + boolMethod( + "isNetworkConnectivityCaptureEnabled" + ) { automaticDataCaptureConfig?.networkConnectivityServiceEnabled } + boolMethod("isAnrCaptureEnabled") { automaticDataCaptureConfig?.anrServiceEnabled } + boolMethod("isDiskUsageCaptureEnabled") { app?.reportDiskUsage } + boolMethod("isJvmCrashCaptureEnabled") { crashHandler?.enabled } + boolMethod("isAeiCaptureEnabled") { appExitInfoConfig?.aeiCaptureEnabled } + boolMethod("is3rdPartySigHandlerDetectionEnabled") { sigHandlerDetection } + boolMethod("isBackgroundActivityCaptureEnabled") { backgroundActivityConfig?.backgroundActivityCaptureEnabled } + boolMethod("isWebViewBreadcrumbCaptureEnabled") { webViewConfig?.captureWebViews } + boolMethod("isWebViewBreadcrumbQueryParamCaptureEnabled") { webViewConfig?.captureQueryParams } + boolMethod("isFcmPiiDataCaptureEnabled") { captureFcmPiiData } + boolMethod("isRequestContentLengthCaptureEnabled") { networking?.captureRequestContentLength } + boolMethod("isHttpUrlConnectionCaptureEnabled") { networking?.enableNativeMonitoring } + boolMethod("isNetworkSpanForwardingEnabled") { networking?.enableNetworkSpanForwarding } + boolMethod("isUiLoadTracingEnabled") { automaticDataCaptureConfig?.uiLoadPerfTracingDisabled != true } + boolMethod("isUiLoadTracingTraceAll") { + if (automaticDataCaptureConfig != null) { + automaticDataCaptureConfig.uiLoadPerfTracingSelectedOnly != true && + automaticDataCaptureConfig.uiLoadPerfTracingDisabled != true + } else { + true + } + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentation.kt new file mode 100644 index 0000000000..1fd9e660c5 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentation.kt @@ -0,0 +1,19 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.intMethod +import io.embrace.android.gradle.plugin.instrumentation.config.arch.mapMethod +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringListMethod +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringMethod +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createNetworkCaptureConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + intMethod("getRequestLimitPerDomain") { cfg.embraceConfig?.sdkConfig?.networking?.defaultCaptureLimit } + stringListMethod("getIgnoredRequestPatternList") { cfg.embraceConfig?.sdkConfig?.networking?.disabledUrlPatterns } + stringMethod("getNetworkBodyCapturePublicKey") { cfg.embraceConfig?.sdkConfig?.capturePublicKey } + mapMethod("getLimitsByDomain") { + cfg.embraceConfig?.sdkConfig?.networking?.domains?.associate { + it.domain to it.limit.toString() + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentation.kt new file mode 100644 index 0000000000..52686d625e --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentation.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringMethod +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createProjectConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + stringMethod("getAppId") { cfg.embraceConfig?.appId } + stringMethod("getAppFramework") { cfg.embraceConfig?.sdkConfig?.appFramework } + stringMethod("getBuildId") { cfg.buildId } + stringMethod("getBuildFlavor") { cfg.buildFlavor } + stringMethod("getBuildType") { cfg.buildType } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentation.kt new file mode 100644 index 0000000000..e227cf4d22 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentation.kt @@ -0,0 +1,9 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringListMethod +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createRedactionConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + stringListMethod("getSensitiveKeysDenylist") { cfg.embraceConfig?.sdkConfig?.sensitiveKeysDenylist } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentation.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentation.kt new file mode 100644 index 0000000000..52d9a170ba --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentation.kt @@ -0,0 +1,10 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.modelSdkConfigClass +import io.embrace.android.gradle.plugin.instrumentation.config.arch.stringListMethod +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig + +fun createSessionConfigInstrumentation(cfg: VariantConfig) = modelSdkConfigClass { + stringListMethod("getSessionComponents") { cfg.embraceConfig?.sdkConfig?.sessionConfig?.sessionComponents } + stringListMethod("getFullSessionEvents") { cfg.embraceConfig?.sdkConfig?.sessionConfig?.fullSessionEvents } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AnrLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AnrLocalConfig.kt new file mode 100644 index 0000000000..19e742f0ca --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AnrLocalConfig.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class AnrLocalConfig( + @Json(name = "capture_unity_thread") + val captureUnityThread: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppExitInfoLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppExitInfoLocalConfig.kt new file mode 100644 index 0000000000..ec93f57e28 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppExitInfoLocalConfig.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class AppExitInfoLocalConfig( + @Json(name = "aei_enabled") + val aeiCaptureEnabled: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppLocalConfig.kt new file mode 100644 index 0000000000..c05a46000b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AppLocalConfig.kt @@ -0,0 +1,18 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class AppLocalConfig( + + @Json(name = "report_disk_usage") + val reportDiskUsage: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AutomaticDataCaptureLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AutomaticDataCaptureLocalConfig.kt new file mode 100644 index 0000000000..e1261d6e42 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/AutomaticDataCaptureLocalConfig.kt @@ -0,0 +1,32 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class AutomaticDataCaptureLocalConfig( + @Json(name = "memory_info") + val memoryServiceEnabled: Boolean? = null, + + @Json(name = "power_save_mode_info") + val powerSaveModeServiceEnabled: Boolean? = null, + + @Json(name = "network_connectivity_info") + val networkConnectivityServiceEnabled: Boolean? = null, + + @Json(name = "anr_info") + val anrServiceEnabled: Boolean? = null, + + @Json(name = "ui_load_tracing_disabled") + val uiLoadPerfTracingDisabled: Boolean? = null, + + @Json(name = "ui_load_tracing_selected_only") + val uiLoadPerfTracingSelectedOnly: Boolean? = null, +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BackgroundActivityLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BackgroundActivityLocalConfig.kt new file mode 100644 index 0000000000..bd7e073e90 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BackgroundActivityLocalConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents the background activity configuration element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class BackgroundActivityLocalConfig( + @Json(name = "capture_enabled") + val backgroundActivityCaptureEnabled: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BaseUrlLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BaseUrlLocalConfig.kt new file mode 100644 index 0000000000..1d4fb3c409 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/BaseUrlLocalConfig.kt @@ -0,0 +1,23 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents the base URLs element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class BaseUrlLocalConfig( + @Json(name = "config") + val config: String? = null, + + @Json(name = "data") + val data: String? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ComposeLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ComposeLocalConfig.kt new file mode 100644 index 0000000000..4adb44b148 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ComposeLocalConfig.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class ComposeLocalConfig( + @Json(name = "capture_compose_onclick") + val captureComposeOnClick: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/CrashHandlerLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/CrashHandlerLocalConfig.kt new file mode 100644 index 0000000000..bbd76de3d7 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/CrashHandlerLocalConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents the crash handler element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class CrashHandlerLocalConfig( + @Json(name = "enabled") + val enabled: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/DomainLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/DomainLocalConfig.kt new file mode 100644 index 0000000000..bf35b26bb4 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/DomainLocalConfig.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents each domain element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class DomainLocalConfig( + + /** + * Url for the domain. + */ + @Json(name = "domain_name") + val domain: String, + + /** + * Limit for the number of requests to be tracked. + */ + @Json(name = "domain_limit") + val limit: Int +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/NetworkLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/NetworkLocalConfig.kt new file mode 100644 index 0000000000..2b2eef7657 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/NetworkLocalConfig.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents the networking configuration element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class NetworkLocalConfig( + + /** + * The default capture limit for the specified domains. + */ + @Json(name = "default_capture_limit") + val defaultCaptureLimit: Int? = null, + + @Json(name = "domains") + val domains: List? = null, + + @Json(name = "capture_request_content_length") + val captureRequestContentLength: Boolean? = null, + + @Json(name = "disabled_url_patterns") + val disabledUrlPatterns: List? = null, + + @Json(name = "enable_native_monitoring") + val enableNativeMonitoring: Boolean? = null, + + @Json(name = "enable_network_span_forwarding") + val enableNetworkSpanForwarding: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt new file mode 100644 index 0000000000..92c0a5b7ca --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SdkLocalConfig.kt @@ -0,0 +1,116 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class SdkLocalConfig( + /** + * Service enablement config settings + */ + @Json(name = "automatic_data_capture") + val automaticDataCaptureConfig: AutomaticDataCaptureLocalConfig? = null, + + /** + * Taps + */ + @Json(name = "taps") + val taps: TapsLocalConfig? = null, + + /** + * View settings + */ + @Json(name = "view_config") + val viewConfig: ViewLocalConfig? = null, + + /** + * Webview settings + */ + @Json(name = "webview") + val webViewConfig: WebViewLocalConfig? = null, + + /** + * Crash handler settings + */ + @Json(name = "crash_handler") + val crashHandler: CrashHandlerLocalConfig? = null, + + /** + * Compose settings + */ + @Json(name = "compose") + val composeConfig: ComposeLocalConfig? = null, + + /** + * Whether fcm PII data should be hidden or not + */ + @Json(name = "capture_fcm_pii_data") + val captureFcmPiiData: Boolean? = null, + + /** + * Networking moment settings + */ + @Json(name = "networking") + val networking: NetworkLocalConfig? = null, + + @Json(name = "capture_public_key") + val capturePublicKey: String? = null, + + /** + * List of strings for sensitive keys that should be redacted when they are sent to the server. + */ + @Json(name = "sensitive_keys_denylist") + val sensitiveKeysDenylist: List? = null, + + /** + * ANR settings + */ + @Json(name = "anr") + val anr: AnrLocalConfig? = null, + + /** + * App settings + */ + @Json(name = "app") + val app: AppLocalConfig? = null, + + /** + * Background activity config settings + */ + @Json(name = "background_activity") + val backgroundActivityConfig: BackgroundActivityLocalConfig? = null, + + /** + * Base URL settings + */ + @Json(name = "base_urls") + val baseUrls: BaseUrlLocalConfig? = null, + + /** + * Session config settings + */ + @Json(name = "session") + val sessionConfig: SessionLocalConfig? = null, + + /** + * Whether signal handler detection should be enabled or not + */ + @Json(name = "sig_handler_detection") + val sigHandlerDetection: Boolean? = null, + + /** + * Background activity config settings + */ + @Json(name = "app_exit_info") + val appExitInfoConfig: AppExitInfoLocalConfig? = null, + + @Json(name = "app_framework") + val appFramework: String? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SessionLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SessionLocalConfig.kt new file mode 100644 index 0000000000..ba71d417dc --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/SessionLocalConfig.kt @@ -0,0 +1,33 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Represents the session configuration element specified in the Embrace config file. + */ +@JsonClass(generateAdapter = true) +data class SessionLocalConfig( + + /** + * A whitelist of session components (i.e. Breadcrumbs, Session properties, etc) that should be + * included in the session payload. The presence of this property denotes that the gating + * feature is enabled. + */ + @Json(name = "components") + val sessionComponents: List? = null, + + /** + * A list of events (crashes, errors, etc) allowed to send a full session payload if the + * gating feature is enabled. + */ + @Json(name = "send_full_for") + val fullSessionEvents: List? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/TapsLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/TapsLocalConfig.kt new file mode 100644 index 0000000000..6a8717678a --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/TapsLocalConfig.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class TapsLocalConfig( + @Json(name = "capture_coordinates") + val captureCoordinates: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/UnityConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/UnityConfig.kt new file mode 100644 index 0000000000..c861e5ff5c --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/UnityConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +/** + * Unity-specific configuration. + */ +@JsonClass(generateAdapter = true) +data class UnityConfig( + @Json(name = "symbols_archive_name") + val symbolsArchiveName: String? +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/VariantConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/VariantConfig.kt new file mode 100644 index 0000000000..1ee23e7612 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/VariantConfig.kt @@ -0,0 +1,73 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import java.io.Serializable + +/** + * This data class holds all configuration from Embrace and Android that is dependent on the + * variant being built. + */ +@JsonClass(generateAdapter = true) +data class VariantConfig( + val variantName: String? = null, + val variantVersion: String? = null, + val buildId: String? = null, + val buildType: String? = null, + val buildFlavor: String? = null, + val embraceConfig: EmbraceVariantConfig? = null +) : Serializable { + + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + + /** + * It builds a full configuration for a variant. + */ + fun from( + embraceVariantConfig: EmbraceVariantConfig?, + androidVariantConfig: AndroidCompactedVariantData + ) = + VariantConfig( + embraceConfig = embraceVariantConfig, + variantName = androidVariantConfig.name, + variantVersion = androidVariantConfig.versionName, + buildType = androidVariantConfig.buildTypeName, + buildFlavor = androidVariantConfig.flavorName, + buildId = androidVariantConfig.buildId, + ) + } +} + +/** + * This data class holds all embrace configuration that is dependent on the variant being built. + */ +@JsonClass(generateAdapter = true) +data class EmbraceVariantConfig( + + @Json(name = "app_id") + val appId: String?, + + @Json(name = "api_token") + val apiToken: String?, + + @Json(name = "ndk_enabled") + val ndkEnabled: Boolean?, + + @Json(name = "sdk_config") + val sdkConfig: SdkLocalConfig?, + + @Json(name = "unity") + val unityConfig: UnityConfig?, + + val configStr: String? = null, + +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ViewLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ViewLocalConfig.kt new file mode 100644 index 0000000000..ac57e43dae --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/ViewLocalConfig.kt @@ -0,0 +1,18 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class ViewLocalConfig( + + @Json(name = "enable_automatic_activity_capture") + val enableAutomaticActivityCapture: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/WebViewLocalConfig.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/WebViewLocalConfig.kt new file mode 100644 index 0000000000..3352ea4867 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/model/WebViewLocalConfig.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class WebViewLocalConfig( + @Json(name = "enable") + val captureWebViews: Boolean? = null, + + @Json(name = "capture_query_params") + val captureQueryParams: Boolean? = null +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/visitor/ConfigInstrumentationClassVisitor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/visitor/ConfigInstrumentationClassVisitor.kt new file mode 100644 index 0000000000..202bbf7478 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/config/visitor/ConfigInstrumentationClassVisitor.kt @@ -0,0 +1,26 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.visitor + +import io.embrace.android.gradle.plugin.instrumentation.config.arch.InstrumentedConfigClass +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Instruments the config classes with defaults from embrace-config.json. + */ +class ConfigInstrumentationClassVisitor( + private val instrumentedConfigClass: InstrumentedConfigClass, + api: Int, + cv: ClassVisitor? +) : ClassVisitor(api, cv) { + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val visitor = super.visitMethod(access, name, descriptor, signature, exceptions) + return instrumentedConfigClass.getMethodVisitor(name, descriptor, api, visitor) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ClassVisitFilter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ClassVisitFilter.kt new file mode 100644 index 0000000000..a4446ed1ed --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/ClassVisitFilter.kt @@ -0,0 +1,12 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext + +@Suppress("UnstableApiUsage") +interface ClassVisitFilter { + + /** + * Returns true if a visitor wants to visit a given class context. + */ + fun accept(classContext: ClassContext): Boolean +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceClassAdapter.kt new file mode 100644 index 0000000000..fc8d230e03 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceClassAdapter.kt @@ -0,0 +1,46 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Visits the [FirebaseMessagingService] class and returns a [FirebaseMessagingServiceMethodAdapter] + * for the onMessageReceived method. + */ +class FirebaseMessagingServiceClassAdapter( + api: Int, + internal val nextClassVisitor: ClassVisitor?, + private val logger: (() -> String) -> Unit +) : ClassVisitor(api, nextClassVisitor) { // TODO add tests + + companion object : ClassVisitFilter { + private const val CLASS_NAME = "com.google.firebase.messaging.FirebaseMessagingService" + private const val METHOD_NAME = "onMessageReceived" + private const val METHOD_DESC = "(Lcom/google/firebase/messaging/RemoteMessage;)V" + + @Suppress("UnstableApiUsage") + override fun accept(classContext: ClassContext): Boolean { + if (classContext.currentClassData.superClasses.contains(CLASS_NAME)) { + return true + } + return false + } + } + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME == name && METHOD_DESC == desc) { + logger { "FirebaseMessagingServiceClassAdapter: instrumented method $name $desc" } + FirebaseMessagingServiceMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceMethodAdapter.kt new file mode 100644 index 0000000000..fde43f804d --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/FirebaseMessagingServiceMethodAdapter.kt @@ -0,0 +1,29 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits the onMessageReceived method and inserts a call to + * FirebaseSwazzledHooks._onMessageReceived at the very start of the method. + */ +class FirebaseMessagingServiceMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load local variable 'remoteMessage' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 1) + // invoke FirebaseSwazzledHooks._onMessageReceived() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/fcm/swazzle/callback/com/android/fcm/FirebaseSwazzledHooks", + "_onMessageReceived", + "(Lcom/google/firebase/messaging/RemoteMessage;)V", + false + ) + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapter.kt new file mode 100644 index 0000000000..4aac92acce --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapter.kt @@ -0,0 +1,42 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Visits a class and returns an [OkHttpInitMethodAdapter] for OkHttp$Builder methods. + */ +class OkHttpClassAdapter( + api: Int, + internal val nextClassVisitor: ClassVisitor?, + private val logger: (() -> String) -> Unit +) : ClassVisitor(api, nextClassVisitor) { + + companion object : ClassVisitFilter { + private const val CLASS_NAME = "okhttp3.OkHttpClient\$Builder" + private const val METHOD_NAME_BUILD = "build" + private const val METHOD_DESC_BUILD = "()Lokhttp3/OkHttpClient;" + + override fun accept(classContext: ClassContext): Boolean { + return classContext.currentClassData.className == CLASS_NAME + } + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME_BUILD == name && METHOD_DESC_BUILD == desc) { + logger { "OkHttpClassAdapter: instrumented method $name $desc" } + OkHttpMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpMethodAdapter.kt new file mode 100644 index 0000000000..698468d1b5 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpMethodAdapter.kt @@ -0,0 +1,31 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits OkHttp methods and inserts a call to OkHttpClient$Builder._preBuild at the very start + * of the method. + */ +class OkHttpMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load local variable 'this' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // invoke Builder._preBuild(this) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient\$Builder", + "_preBuild", + "(Lokhttp3/OkHttpClient\$Builder;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapter.kt new file mode 100644 index 0000000000..1b4ac8dec6 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapter.kt @@ -0,0 +1,43 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Visits a class and returns an [OnClickMethodAdapter] for any onClick method which + * conforms to the OnClickListener interface. + */ +class OnClickClassAdapter( + api: Int, + internal val nextClassVisitor: ClassVisitor?, + private val logger: (() -> String) -> Unit +) : ClassVisitor(api, nextClassVisitor) { + + companion object : ClassVisitFilter { + private const val METHOD_NAME = "onClick" + private const val METHOD_DESC = "(Landroid/view/View;)V" + + override fun accept(classContext: ClassContext) = true + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME == name && METHOD_DESC == desc && !isStatic(access)) { + logger { "OnClickClassAdapter: instrumented method $name $desc" } + OnClickMethodAdapter(api, nextMethodVisitor) + } else if (METHOD_DESC == desc && isStatic(access) && isSynthetic(access)) { + logger { "OnClickClassAdapter: instrumented synthetic method $name $desc" } + OnClickStaticMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickMethodAdapter.kt new file mode 100644 index 0000000000..63d220c91d --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickMethodAdapter.kt @@ -0,0 +1,34 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits an onClick method and inserts a call to ViewSwazzledHooks._preOnClick at the very start + * of the method. + */ +class OnClickMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load local variable 'this' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 1) + + // invoke ViewSwazzledHooks$OnClickListener._preOnClick() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnClickListener", + "_preOnClick", + "(Landroid/view/View\$OnClickListener;Landroid/view/View;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticMethodAdapter.kt new file mode 100644 index 0000000000..f98f94480f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticMethodAdapter.kt @@ -0,0 +1,37 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a static onClick method and inserts a call to ViewSwazzledHooks._preOnClick at the very + * start of the method. + */ +class OnClickStaticMethodAdapter( + api: Int, + methodVisitor: MethodVisitor?, +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load the null reference and push it onto the operand stack. + // null is ok here as _preOnClick doesn't use the listener, + // and in a static context 'this' does not actually implement OnClickListener + // in Java bytecode. + visitInsn(Opcodes.ACONST_NULL) + + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // invoke ViewSwazzledHooks$OnClickListener._preOnClick() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnClickListener", + "_preOnClick", + "(Landroid/view/View\$OnClickListener;Landroid/view/View;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapter.kt new file mode 100644 index 0000000000..69a494e9c7 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapter.kt @@ -0,0 +1,43 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor + +/** + * Visits a class and returns an [OnLongClickMethodAdapter] for any onLongClick method which + * conforms to the OnLongClickListener interface. + */ +class OnLongClickClassAdapter( + api: Int, + internal val nextClassVisitor: ClassVisitor?, + private val logger: (() -> String) -> Unit +) : ClassVisitor(api, nextClassVisitor) { + + companion object : ClassVisitFilter { + private const val METHOD_NAME = "onLongClick" + private const val METHOD_DESC = "(Landroid/view/View;)Z" + + override fun accept(classContext: ClassContext) = true + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME == name && METHOD_DESC == desc && !isStatic(access)) { + logger { "OnLongClickClassAdapter: instrumented method $name $desc" } + OnLongClickMethodAdapter(api, nextMethodVisitor) + } else if (METHOD_DESC == desc && isStatic(access) && isSynthetic(access)) { + logger { "OnLongClickClassAdapter: instrumented synthetic method $name $desc" } + OnLongClickStaticMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickMethodAdapter.kt new file mode 100644 index 0000000000..48197e79cb --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickMethodAdapter.kt @@ -0,0 +1,34 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits an onLongClick method and inserts a call to ViewSwazzledHooks._preOnLongClick at + * the very start of the method. + */ +class OnLongClickMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load local variable 'this' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 1) + + // invoke ViewSwazzledHooks$OnLongClickListener._preOnLongClick() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnLongClickListener", + "_preOnLongClick", + "(Landroid/view/View\$OnLongClickListener;Landroid/view/View;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticMethodAdapter.kt new file mode 100644 index 0000000000..dded713ade --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticMethodAdapter.kt @@ -0,0 +1,37 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits a static onLongClick method and inserts a call to ViewSwazzledHooks._preOnLongClick + * at the very start of the method. + */ +class OnLongClickStaticMethodAdapter( + api: Int, + methodVisitor: MethodVisitor?, +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + // load the null reference and push it onto the operand stack. + // null is ok here as _preOnLongClick doesn't use the listener, + // and in a static context 'this' does not actually implement OnLongClickListener + // in Java bytecode. + visitInsn(Opcodes.ACONST_NULL) + + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // invoke ViewSwazzledHooks$OnLongClickListener._preOnLongClick() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnLongClickListener", + "_preOnLongClick", + "(Landroid/view/View\$OnLongClickListener;Landroid/view/View;)V", + false + ) + + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OpcodeExtensions.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OpcodeExtensions.kt new file mode 100644 index 0000000000..eb69dc3ce6 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OpcodeExtensions.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.Opcodes + +/** + * Returns true if a method is static. + */ +internal fun isStatic(access: Int) = access.and(Opcodes.ACC_STATIC) != 0 + +/** + * Returns true if a method is synthetic (e.g. a Java lambda). + */ +internal fun isSynthetic(access: Int) = access.and(Opcodes.ACC_SYNTHETIC) != 0 diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapter.kt new file mode 100644 index 0000000000..1ed5cbab67 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapter.kt @@ -0,0 +1,68 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import com.android.build.api.instrumentation.ClassContext +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits the [WebViewClient] class and returns a [WebViewClientMethodAdapter] for the + * onPageStarted method. + */ +class WebViewClientClassAdapter( + api: Int, + internal val nextClassVisitor: ClassVisitor?, + private val logger: (() -> String) -> Unit +) : ClassVisitor(api, nextClassVisitor) { + + companion object : ClassVisitFilter { + private const val CLASS_NAME = "android.webkit.WebViewClient" + private const val METHOD_NAME = "onPageStarted" + private const val METHOD_DESC = + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V" + + override fun accept(classContext: ClassContext): Boolean { + if (classContext.currentClassData.superClasses.contains(CLASS_NAME)) { + return true + } + return false + } + } + + var hasOverride = false + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor? { + val nextMethodVisitor = super.visitMethod(access, name, desc, signature, exceptions) + + return if (METHOD_NAME == name && METHOD_DESC == desc) { + logger { "WebViewClientClassAdapter: instrumented method $name $desc" } + hasOverride = true + WebViewClientMethodAdapter(api, nextMethodVisitor) + } else { + nextMethodVisitor + } + } + + override fun visitEnd() { + // add an override of onPageStarted if the class does not have one already. + if (!hasOverride) { + logger { "WebViewClientClassAdapter: instrumented method $METHOD_NAME $METHOD_DESC (added override)" } + + val nextMethodVisitor = super.visitMethod( + Opcodes.ACC_PUBLIC, + METHOD_NAME, + METHOD_DESC, + null, + emptyArray() + ) + WebViewClientOverrideMethodAdapter(api, nextMethodVisitor).visitEnd() + } + super.visitEnd() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapter.kt new file mode 100644 index 0000000000..6bc13b6ec2 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapter.kt @@ -0,0 +1,40 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Visits the onPageStarted method and inserts a call to ViewSwazzledHooks._preOnPageStarted + * at the very start of the method. + */ +open class WebViewClientMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : MethodVisitor(api, methodVisitor) { + + override fun visitCode() { + instrumentOnPageStarted() + // call super last to reduce chance of interference with other bytecode instrumentation + super.visitCode() + } + + internal fun instrumentOnPageStarted() { + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 1) + + // load local variable 'url' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 2) + + // load local variable 'favicon' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 3) + + // invoke WebViewClientSwazzledHooks._preOnPageStarted() + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/WebViewClientSwazzledHooks", + "_preOnPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + false + ) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideMethodAdapter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideMethodAdapter.kt new file mode 100644 index 0000000000..be929c63fe --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideMethodAdapter.kt @@ -0,0 +1,44 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * Creates an onPageStarted method override and inserts a call to + * ViewSwazzledHooks._preOnPageStarted at the very start of the method. + */ +class WebViewClientOverrideMethodAdapter( + api: Int, + methodVisitor: MethodVisitor? +) : WebViewClientMethodAdapter(api, methodVisitor) { + + override fun visitEnd() { + instrumentOnPageStarted() + addSuperCall() + super.visitEnd() + visitInsn(Opcodes.RETURN) + visitMaxs(4, 0) + } + + private fun addSuperCall() { + // load local variable 'this' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 0) + + // load local variable 'view' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 1) + + // load local variable 'url' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 2) + + // load local variable 'favicon' and push it onto the operand stack + visitVarInsn(Opcodes.ALOAD, 3) + + visitMethodInsn( + Opcodes.INVOKESPECIAL, + "android/webkit/WebViewClient", + "onPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + false + ) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/model/AndroidCompactedVariantData.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/model/AndroidCompactedVariantData.kt new file mode 100644 index 0000000000..69fcdff601 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/model/AndroidCompactedVariantData.kt @@ -0,0 +1,44 @@ +package io.embrace.android.gradle.plugin.model + +import com.android.build.api.variant.Variant +import io.embrace.android.gradle.plugin.util.UuidUtils +import java.io.Serializable + +/** + * It represents all needed data from a Variant. + */ +data class AndroidCompactedVariantData( + val name: String, + val flavorName: String, + val buildTypeName: String, + val isBuildTypeDebuggable: Boolean, + val versionName: String?, + val productFlavors: List, + val sourceMapPath: String, + val buildId: String = UuidUtils.generateEmbraceUuid() +) : Serializable { + + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + + fun from(variant: Variant): AndroidCompactedVariantData { + val buildtype = variant.buildType?.lowercase() + val debuggable = buildtype?.contains("debug") ?: false + return AndroidCompactedVariantData( + variant.name, + variant.flavorName ?: "", + variant.buildType ?: "", + debuggable, + "", // not used for now + fetchProductFlavors(variant), + variant.name + ) + } + + private fun fetchProductFlavors(variant: Variant) = + variant.productFlavors.map { + it.second + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/EmbraceEndpoint.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/EmbraceEndpoint.kt new file mode 100644 index 0000000000..8fcbad0d8b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/EmbraceEndpoint.kt @@ -0,0 +1,11 @@ +package io.embrace.android.gradle.plugin.network + +enum class EmbraceEndpoint(val url: String) { + BUILD_DATA("/v2/debug/android/build"), + SOURCE_MAP("/v2/store/sourcemap"), + NDK_HANDSHAKE("/v2/store/ndk/handshake"), + NDK("/v2/store/ndk"), + PROGUARD("/v2/store/proguard"), + METHOD_MAP("/v2/store/methodmap"), + LINE_MAP("/v2/store/linemap") +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpCallResult.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpCallResult.kt new file mode 100644 index 0000000000..6cb8af6b4e --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpCallResult.kt @@ -0,0 +1,7 @@ +package io.embrace.android.gradle.plugin.network + +sealed class HttpCallResult { + data class Success(val body: T?, val code: Int) : HttpCallResult() + data class Failure(val errorMessage: String?, val code: Int) : HttpCallResult() + data class Error(val exception: Exception) : HttpCallResult() +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpStatus.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpStatus.kt new file mode 100644 index 0000000000..f9901b5d52 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/HttpStatus.kt @@ -0,0 +1,9 @@ +package io.embrace.android.gradle.plugin.network + +/** + * Constants enumerating the HTTP status codes + */ +const val SC_FORBIDDEN = 403 +const val SC_BAD_REQUEST = 400 +const val SC_NOT_FOUND = 404 +const val SC_OK = 200 diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/NetworkService.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/NetworkService.kt new file mode 100644 index 0000000000..d5b15151c2 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/NetworkService.kt @@ -0,0 +1,31 @@ +package io.embrace.android.gradle.plugin.network + +import io.embrace.android.gradle.plugin.buildreporter.BuildTelemetryRequest +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeRequest +import java.io.File + +/** + * Interface used to define a contract for network requests and response callbacks. + */ +interface NetworkService { + + fun postBuildTelemetry(request: BuildTelemetryRequest): HttpCallResult + + fun postNdkHandshake( + appId: String, + handshake: NdkUploadHandshakeRequest + ): HttpCallResult + + fun uploadFile(params: RequestParams, file: File): HttpCallResult + + fun uploadRnSourcemapFile(params: RequestParams, file: File): HttpCallResult + + fun uploadNdkSymbolFile( + params: RequestParams, + file: File, + variantName: String, + arch: String, + id: String, + ): HttpCallResult +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/OkHttpNetworkService.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/OkHttpNetworkService.kt new file mode 100644 index 0000000000..a7eea7aa53 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/OkHttpNetworkService.kt @@ -0,0 +1,226 @@ +package io.embrace.android.gradle.plugin.network + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.buildreporter.BuildTelemetryRequest +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeRequest +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeResponse +import io.embrace.android.gradle.plugin.util.serialization.EmbraceSerializer +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.Response +import java.io.File +import java.io.InputStream +import java.util.concurrent.TimeUnit + +const val HEADER_APP_ID = "X-EM-AID" +private const val KEY_APP_ID = "app" +private const val KEY_API_TOKEN = "token" +private const val KEY_BUILD_ID = "id" +private const val KEY_MAPPING_FILE = "file" +private const val KEY_VARIANT = "variant" +private const val KEY_ARCH = "arch" +private const val KEY_SYMBOL_ID = "id" +private const val KEY_FILENAME = "filename" + +/** + * This class acts as a wrapper for okHttp so the rest of the project is decoupled from the networking library + * being used. + */ +class OkHttpNetworkService( + private val baseUrl: String, +) : NetworkService { + + @Transient + private val client = OkHttpClient().newBuilder() + .writeTimeout(NETWORK_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build() + + private val logger = Logger(OkHttpNetworkService::class.java) + + private val mediaTypeJson = "application/json".toMediaType() + private val mediaTypeText = "text/plain".toMediaType() + private val serializer: ThreadLocal = + object : ThreadLocal() { + override fun initialValue(): EmbraceSerializer { + return MoshiSerializer() + } + } + private val defaultBodyDeserializer = { stream: InputStream -> + stream.bufferedReader().readText() + } + + override fun postBuildTelemetry(request: BuildTelemetryRequest): HttpCallResult { + return makePostRequest( + endpoint = EmbraceEndpoint.BUILD_DATA, + payload = request, + deserializationAction = defaultBodyDeserializer + ) + } + + override fun postNdkHandshake( + appId: String, + handshake: NdkUploadHandshakeRequest + ): HttpCallResult { + return makePostRequest( + endpoint = EmbraceEndpoint.NDK_HANDSHAKE, + payload = handshake, + appId = appId, + ) { serializer.get().fromJson(it, NdkUploadHandshakeResponse::class.java) } + } + + override fun uploadNdkSymbolFile( + params: RequestParams, + file: File, + variantName: String, + arch: String, + id: String + ): HttpCallResult { + return makeMultipartRequest(params, file) { + addFormDataPart(KEY_VARIANT, variantName) + addFormDataPart(KEY_ARCH, arch) + addFormDataPart(KEY_SYMBOL_ID, id) + params.fileName?.let { + addFormDataPart(KEY_FILENAME, params.fileName) + } + } + } + + override fun uploadRnSourcemapFile(params: RequestParams, file: File): HttpCallResult { + return makeMultipartRequest(params, file) + } + + override fun uploadFile(params: RequestParams, file: File): HttpCallResult { + return makeMultipartRequest(params, file) + } + + private fun makeMultipartRequest( + params: RequestParams, + file: File, + action: MultipartBody.Builder.() -> Unit = {} + ): HttpCallResult { + return makeRequest( + requestProvider = { + val requestBody = prepareCommonMultipartBody(params, file) + action(requestBody) + prepareCommonRequest(params.endpoint, params.appId) + .post(requestBody.build()) + .build() + }, + deserializationAction = defaultBodyDeserializer + ) + } + + private inline fun makePostRequest( + endpoint: EmbraceEndpoint, + payload: T, + appId: String? = null, + deserializationAction: (stream: InputStream) -> O + ): HttpCallResult { + val body = StreamedRequestBody(mediaTypeJson) { + serializer.get().toJson(payload, T::class.java, it) + } + return makeRequest( + requestProvider = { + prepareCommonRequest(endpoint, appId) + .post(body) + .build() + }, + deserializationAction = deserializationAction + ) + } + + private fun prepareCommonRequest( + endpoint: EmbraceEndpoint, + appId: String?, + ): Request.Builder { + val builder = Request.Builder() + builder.url("$baseUrl${endpoint.url}") + appId?.isNotBlank()?.let { + builder.addHeader(HEADER_APP_ID, appId) + } + return builder + } + + private inline fun makeRequest( + requestProvider: () -> Request, + deserializationAction: (stream: InputStream) -> O, + ): HttpCallResult { + return try { + val request = requestProvider() + client.newCall(request).execute().use { response -> + val result = handleServerResponse(response, deserializationAction) + logHttpCallResult(result) + result + } + } catch (exc: Exception) { + logger.error("Exception occurred while making network request", exc) + HttpCallResult.Error(exc) + } + } + + private fun logHttpCallResult(result: HttpCallResult) { + when (result) { + is HttpCallResult.Success<*> -> { + logger.info("Request succeeded code=${result.code}, body=${result.body}") + } + + is HttpCallResult.Failure -> { + logger.error("Request failed: code=${result.code}, message=${result.errorMessage}") + } + + is HttpCallResult.Error -> { + logger.error( + "Failed to make request", + result.exception + ) + } + } + } + + private inline fun handleServerResponse( + response: Response, + deserializationAction: (stream: InputStream) -> O + ): HttpCallResult { + val result = when { + response.isSuccessful -> { + val payload = + response.body?.byteStream()?.buffered()?.use(deserializationAction) + HttpCallResult.Success(payload, response.code) + } + + else -> { + HttpCallResult.Failure(response.body?.string(), response.code) + } + } + return result + } + + private fun prepareCommonMultipartBody( + params: RequestParams, + file: File + ): MultipartBody.Builder { + return MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart(KEY_APP_ID, params.appId) + .addFormDataPart(KEY_API_TOKEN, params.apiToken) + .apply { + if (!params.buildId.isNullOrBlank()) { + addFormDataPart(KEY_BUILD_ID, params.buildId) + } + } + .addFormDataPart( + KEY_MAPPING_FILE, + params.fileName, + file.asRequestBody(mediaTypeText) + ) + } + + companion object { + private const val NETWORK_WRITE_TIMEOUT_SECONDS = 120L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/StreamedRequestBody.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/StreamedRequestBody.kt new file mode 100644 index 0000000000..d15908ab3f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/network/StreamedRequestBody.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.network + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import java.io.OutputStream + +class StreamedRequestBody( + private val mediaType: MediaType, + private val serializationAction: (stream: OutputStream) -> Unit +) : RequestBody() { + override fun contentType() = mediaType + + override fun writeTo(sink: BufferedSink) { + sink.outputStream().buffered().use(serializationAction) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Clock.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Clock.kt new file mode 100644 index 0000000000..4d18583cb8 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Clock.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.plugin.system + +interface Clock { + /** + * Returns the current milliseconds from epoch. + */ + fun now(): Long +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Environment.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Environment.kt new file mode 100644 index 0000000000..52f5aab925 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/Environment.kt @@ -0,0 +1,11 @@ +package io.embrace.android.gradle.plugin.system + +/** + * It is a wrapper class to get environment variables. + */ +fun interface Environment { + /** + * It gets an environment variable. + */ + fun getVariable(name: String): String? +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemClock.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemClock.kt new file mode 100644 index 0000000000..0fbd1f923f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemClock.kt @@ -0,0 +1,6 @@ +package io.embrace.android.gradle.plugin.system + +class SystemClock : Clock { + + override fun now() = System.currentTimeMillis() +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemWrapper.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemWrapper.kt new file mode 100644 index 0000000000..79cb29c10e --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/system/SystemWrapper.kt @@ -0,0 +1,21 @@ +package io.embrace.android.gradle.plugin.system + +class SystemWrapper { + fun getProperty(key: String): String? { + return try { + System.getProperty(key) + } catch (e: SecurityException) { + null + } catch (e: IllegalArgumentException) { + null + } + } + + fun getenv(name: String): String? { + return try { + System.getenv(name) + } catch (e: SecurityException) { + null + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/BuildResourceWriter.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/BuildResourceWriter.kt new file mode 100644 index 0000000000..742c32e80a --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/BuildResourceWriter.kt @@ -0,0 +1,38 @@ +package io.embrace.android.gradle.plugin.tasks + +import java.io.File + +/** + * Writes a string resource file to the build directory, where it will be incorporated into the + * APK's resources. + */ +class BuildResourceWriter { + + fun writeBuildInfoFile( + dst: File, + resValues: Map + ) { + val parentFile = dst.parentFile + if (parentFile != null) { + parentFile.mkdirs() + val buildInfoXmlContent = createStringsXmlFile(resValues) + dst.writeText(buildInfoXmlContent) + } + } + + private fun createStringsXmlFile( + resValues: Map + ): String { + val sb = StringBuilder(256) + sb.append("\n") + sb.append("\n") + sb.append("\n") + + for ((key, value) in resValues) { + sb.append(" ").append(value).append("\n") + } + sb.append("\n") + return sb.toString() + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTask.kt new file mode 100644 index 0000000000..3dbf6b59e9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTask.kt @@ -0,0 +1,12 @@ +package io.embrace.android.gradle.plugin.tasks + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.Task +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input + +interface EmbraceTask : Task { + + @get:Input + val variantData: Property +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTaskImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTaskImpl.kt new file mode 100644 index 0000000000..2ca6221cc8 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceTaskImpl.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.tasks + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.DefaultTask +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import javax.inject.Inject + +abstract class EmbraceTaskImpl @Inject constructor( + objectFactory: ObjectFactory +) : DefaultTask(), EmbraceTask { + + @get:Input + override val variantData: Property = + objectFactory.property(AndroidCompactedVariantData::class.java) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTask.kt new file mode 100644 index 0000000000..adb083efae --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTask.kt @@ -0,0 +1,15 @@ +package io.embrace.android.gradle.plugin.tasks + +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input + +/** + * Marker interface that contains the bare minimum properties required for tasks used in the + * Embrace gradle plugin. + */ +interface EmbraceUploadTask : EmbraceTask { + + @get:Input + val requestParams: Property +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTaskImpl.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTaskImpl.kt new file mode 100644 index 0000000000..51f9be4308 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/EmbraceUploadTaskImpl.kt @@ -0,0 +1,22 @@ +package io.embrace.android.gradle.plugin.tasks + +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import org.gradle.api.DefaultTask +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import javax.inject.Inject + +abstract class EmbraceUploadTaskImpl @Inject constructor( + objectFactory: ObjectFactory +) : DefaultTask(), EmbraceUploadTask { + + @get:Input + override val requestParams: Property = + objectFactory.property(RequestParams::class.java) + + @get:Input + override val variantData: Property = + objectFactory.property(AndroidCompactedVariantData::class.java) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/FileCompressionTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/FileCompressionTask.kt new file mode 100644 index 0000000000..7a67c824b2 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/FileCompressionTask.kt @@ -0,0 +1,35 @@ +package io.embrace.android.gradle.plugin.tasks.common + +import io.embrace.android.gradle.plugin.tasks.EmbraceTask +import io.embrace.android.gradle.plugin.tasks.EmbraceTaskImpl +import io.embrace.android.gradle.plugin.util.compression.FileCompressor +import io.embrace.android.gradle.plugin.util.compression.ZstdFileCompressor +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject + +/** + * Task that takes a file as an input and compresses it with zstd as an output. + */ +abstract class FileCompressionTask @Inject constructor( + objectFactory: ObjectFactory +) : EmbraceTaskImpl(objectFactory), EmbraceTask { + + private val compressor: FileCompressor = ZstdFileCompressor() + + @get:SkipWhenEmpty + @get:InputFiles + val originalFile: RegularFileProperty = objectFactory.fileProperty() + + @get:OutputFile + val compressedFile: RegularFileProperty = objectFactory.fileProperty() + + @TaskAction + fun onRun() { + compressor.compress(originalFile.asFile.get(), compressedFile.asFile.get()) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/MultipartUploadTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/MultipartUploadTask.kt new file mode 100644 index 0000000000..270e795a29 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/MultipartUploadTask.kt @@ -0,0 +1,28 @@ +package io.embrace.android.gradle.plugin.tasks.common + +import io.embrace.android.gradle.plugin.network.OkHttpNetworkService +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTask +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTaskImpl +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject + +/** + * Task in charge of uploading a compressed file as a multipart request. + */ +abstract class MultipartUploadTask @Inject constructor( + objectFactory: ObjectFactory +) : EmbraceUploadTask, EmbraceUploadTaskImpl(objectFactory) { + + @get:InputFiles + @get:SkipWhenEmpty + val uploadFile: RegularFileProperty = objectFactory.fileProperty() + + @TaskAction + fun onRun() { + OkHttpNetworkService(requestParams.get().baseUrl).uploadFile(requestParams.get(), uploadFile.asFile.get()) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/RequestParams.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/RequestParams.kt new file mode 100644 index 0000000000..7a2bd585d9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/common/RequestParams.kt @@ -0,0 +1,18 @@ +package io.embrace.android.gradle.plugin.tasks.common + +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import java.io.Serializable + +data class RequestParams( + val appId: String, + val apiToken: String, + val endpoint: EmbraceEndpoint, + val baseUrl: String, + val fileName: String? = null, + val buildId: String? = null, +) : Serializable { + companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppInfo.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppInfo.kt new file mode 100644 index 0000000000..46900ee025 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppInfo.kt @@ -0,0 +1,39 @@ +package io.embrace.android.gradle.plugin.tasks.il2cpp + +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint + +/** + * Holds information relating to a single IL2CPP symbol file. + */ +internal sealed class Il2CppInfo( + name: String, + extension: String, + val endpoint: EmbraceEndpoint +) { + + /** + * Information relating to the line number mapping file. + */ + object LineNumberMap : + Il2CppInfo("LineNumberMappings", "json", EmbraceEndpoint.LINE_MAP) + + /** + * Information relating to the method mapping file. + */ + object MethodMap : Il2CppInfo("MethodMap", "tsv", EmbraceEndpoint.METHOD_MAP) + + /** + * Task name used for compressing IL2CPP symbol files. + */ + val compressionTaskName = "il2cppCompressionTask$name" + + /** + * Task name used for uploading IL2CPP symbol files. + */ + val uploadTaskName = "il2cppUploadTask$name" + + /** + * The name of the uncompressed IL2CPP symbol file. + */ + val filename = "$name.$extension" +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppTaskSource.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppTaskSource.kt new file mode 100644 index 0000000000..01979a7a2f --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppTaskSource.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.tasks.il2cpp + +import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadTask +import io.embrace.android.gradle.plugin.util.capitalizedString +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider + +/** + * Fetches the task the IL2CPP compression + upload tasks will depend on. This is currently + * the NDK upload task, because Unity should always be using the NDK to build IL2CPP. + */ +class Il2CppTaskSource { + fun fetchTask(project: Project, variant: AndroidCompactedVariantData): TaskProvider? { + val taskName = "${NdkUploadTask.NAME}${variant.name.capitalizedString()}" + return project.tryGetTaskProvider(taskName) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppUploadTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppUploadTaskRegistration.kt new file mode 100644 index 0000000000..7105623984 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/Il2CppUploadTaskRegistration.kt @@ -0,0 +1,157 @@ +package io.embrace.android.gradle.plugin.tasks.il2cpp + +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.gradle.registerTask +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.tasks.common.FileCompressionTask +import io.embrace.android.gradle.plugin.tasks.common.MultipartUploadTask +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.registration.EmbraceTaskRegistration +import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider +import java.io.File + +/** + * Registers all the tasks that are required to upload IL2CPP symbols. + */ +class Il2CppUploadTaskRegistration : EmbraceTaskRegistration { + + companion object { + + /** + * The output directory for IL2CPP symbol information. Our Unity Editor script copies the + * files to a consistent location for the convenience of the gradle plugin. + */ + private const val IL2CPP_SYMBOLS_DIR = + "unityLibrary/src/main/il2cppOutputProject/Source/il2cppOutput/Symbols" + } + + override fun register(params: RegistrationParams) { + with(params) { + project.afterEvaluate { + val task = Il2CppTaskSource().fetchTask(project, data) ?: return@afterEvaluate + configureIl2CppTasks( + project, + task, + data, + extension, + baseUrl + ) + } + } + } + + private fun configureIl2CppTasks( + project: Project, + ndkTaskProvider: TaskProvider, + variant: AndroidCompactedVariantData, + extension: EmbraceExtensionInternal, + baseUrl: String, + ) { + val il2cppSymbolsDir = File(project.rootDir, IL2CPP_SYMBOLS_DIR) + val variantInfo = extension.variants.getByName(variant.name) + val lineNumberCompressionTaskProvider = configureFileCompressionTask( + project, + variant, + Il2CppInfo.LineNumberMap, + il2cppSymbolsDir, + variantInfo, + ) + configureFileUploadTask( + project, + variant, + Il2CppInfo.LineNumberMap, + ndkTaskProvider, + lineNumberCompressionTaskProvider, + variantInfo, + baseUrl, + ) + + val methodMapCompressionTaskProvider = configureFileCompressionTask( + project, + variant, + Il2CppInfo.MethodMap, + il2cppSymbolsDir, + variantInfo, + ) + configureFileUploadTask( + project, + variant, + Il2CppInfo.MethodMap, + ndkTaskProvider, + methodMapCompressionTaskProvider, + variantInfo, + baseUrl, + ) + } + + private fun configureFileCompressionTask( + project: Project, + variant: AndroidCompactedVariantData, + info: Il2CppInfo, + il2cppSymbolsDir: File, + variantInfo: EmbraceExtensionInternal.VariantExtension, + ): TaskProvider { + val compressionTask = project.registerTask( + info.compressionTaskName, + FileCompressionTask::class.java, + variant + ) { task: FileCompressionTask -> + val fileProvider = project.provider { + File(il2cppSymbolsDir, info.filename) + } + task.onlyIf { fileProvider.get().exists() } + task.originalFile.fileProvider(fileProvider) + task.compressedFile.convention( + project.layout.buildDirectory.file( + "outputs/embrace/il2cpp/compressed/${variantInfo.name}/${info.filename}" + ) + ) + } + return compressionTask + } + + private fun configureFileUploadTask( + project: Project, + variant: AndroidCompactedVariantData, + info: Il2CppInfo, + ndkTaskProvider: TaskProvider, + fileCompressionTask: TaskProvider, + variantInfo: EmbraceExtensionInternal.VariantExtension, + baseUrl: String, + ) { + val uploadTask = project.registerTask( + info.uploadTaskName, + MultipartUploadTask::class.java, + variant + ) { task -> + task.requestParams.set( + project.provider { + RequestParams( + appId = variantInfo.appId.get(), + apiToken = variantInfo.apiToken.get(), + endpoint = info.endpoint, + fileName = info.filename, + buildId = variantInfo.buildId.get(), + baseUrl = baseUrl, + ) + } + ) + + // link output of compression task to the input of this task + // dependencies to mapping file compression will be added automatically + val fileProvider = fileCompressionTask.map { + it.compressedFile.asFile.get() + } + task.onlyIf { fileProvider.get().exists() } + task.uploadFile.fileProvider(fileProvider) + } + + // set us (Embrace) as a dependency of the native obfuscation task + ndkTaskProvider.configure { task -> + task.finalizedBy(uploadTask) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/UnitySymbolFilesManager.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/UnitySymbolFilesManager.kt new file mode 100644 index 0000000000..f1d65bebde --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/il2cpp/UnitySymbolFilesManager.kt @@ -0,0 +1,280 @@ +package io.embrace.android.gradle.plugin.tasks.il2cpp + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.config.UnitySymbolsDir +import io.embrace.android.gradle.plugin.instrumentation.config.model.UnityConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.gradle.api.file.Directory +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.zip.ZipInputStream + +private const val BUFFER_SIZE = 2048 +private const val UNITY_SYMBOLS_NAME = "symbols" +private const val UNITY_SYMBOLS_SUFFIX_NAME = ".zip" +private const val UNITY_SYMBOL_EXTENSION_2019 = ".sym.so" +private const val UNITY_SYMBOL_IL2CPP_EXTENSION_2018 = ".sym" +private const val UNITY_SYMBOLS_DEFAULT_DIR_NAME = "StagingArea" +private const val UNITY_LIBRARY_DEFAULT_DIR_NAME = "unityLibrary" + +internal class UnitySymbolFilesManager { + private val logger = Logger(UnitySymbolFilesManager::class.java) + + companion object Factory { + private lateinit var INSTANCE: UnitySymbolFilesManager + + fun of(): UnitySymbolFilesManager { + if (!::INSTANCE.isInitialized) { + INSTANCE = UnitySymbolFilesManager() + } + + return INSTANCE + } + } + + /** + * Gets the directory where the symbols are located. + * 1. Look for exported unity symbols zip file. + * 2. Look if exported_project/unityLibrary/symbols directory exists. + * 3. Look if Unity temp build directory (StagingArea/Symbols) exists. + */ + fun getSymbolsDir( + realProjectDirectory: Directory, + projectDirectory: Directory, + unityConfig: UnityConfig? + ): UnitySymbolsDir { + val customArchiveName = unityConfig?.symbolsArchiveName?.takeIf { it.isNotEmpty() } + val unitySymbolsArchiveFile = getUnitySymbolsArchive(projectDirectory, customArchiveName) + if (unitySymbolsArchiveFile != null) { + return UnitySymbolsDir(unitySymbolsArchiveFile, true) + } + + // If no archive file is found, return the Unity symbols directory or failing that, the Unity temp directory + val unitySymbolsDir = getUnitySymbolsDirectory( + projectDirectory, + UNITY_LIBRARY_DEFAULT_DIR_NAME + ) ?: getUnitySymbolsDirectory( + realProjectDirectory, + UNITY_SYMBOLS_DEFAULT_DIR_NAME + ) + + return if (unitySymbolsDir != null) { + UnitySymbolsDir(unitySymbolsDir, false) + } else { + logger.error( + "No Unity symbols found for project at path=${realProjectDirectory.asFile.absolutePath}. " + + "The project is not a Unity project or the symbols file was not exported." + ) + UnitySymbolsDir() + } + } + + /** + * Given the directory where the symbols files are stored, get the list of symbol files. + */ + fun getSymbolFiles( + unitySymbolsDir: UnitySymbolsDir, + buildDir: Directory, + variantData: AndroidCompactedVariantData + ): Array { + val symbolsDir = unitySymbolsDir.unitySymbolsDir ?: return emptyArray() + + return if (unitySymbolsDir.isDirPresent() && unitySymbolsDir.zippedSymbols) { + extractSoFilesFromZipFile( + symbolsDir, + buildDir, + variantData + ) + } else { + val files = symbolsDir.listFiles() + if (files == null) { + logger.info("No symbol files found in Unity symbols directory at path: ${symbolsDir.path}") + emptyArray() + } else { + logger.info("Using symbols from Unity symbols directory at path: ${symbolsDir.path}") + files + } + } + } + + /** + * Get Unity exported .zip file with symbols. + */ + private fun getUnitySymbolsArchive( + projectDir: Directory, + customArchiveName: String? + ): File? { + val parentDir = projectDir.asFile.parentFile ?: return null + // Search symbols zip file at same level of exported project folder + val defaultArchiveName = parentDir.name + val projectParent = parentDir.parentFile + val symbolsArchive = projectParent?.searchSymbolsArchive( + defaultArchiveName = defaultArchiveName, + customArchiveName = customArchiveName + ) ?: projectParent.parentFile?.searchSymbolsArchive( + defaultArchiveName = defaultArchiveName, + customArchiveName = customArchiveName + ) + + if (symbolsArchive != null) { + logger.info("Unity symbols archive found at path: ${symbolsArchive.path}") + } + + return symbolsArchive + } + + private fun File.searchSymbolsArchive( + defaultArchiveName: String, + customArchiveName: String? + ): File? { + // Use the last file found that matches the criteria for being a Unity symbols file + var foundFile: File? = null + try { + listFiles()?.forEach { file -> + if (customArchiveName != null && + file.name.startsWith(customArchiveName) && + file.name.endsWith(UNITY_SYMBOLS_SUFFIX_NAME) + ) { + foundFile = file + } else if (file.name.contains(UNITY_SYMBOLS_NAME) && + file.name.endsWith(UNITY_SYMBOLS_SUFFIX_NAME) && + file.name.startsWith(defaultArchiveName) + ) { + foundFile = file + } + } + } catch (ex: Exception) { + logger.warn("Unexpected error searching for Unity symbols archive.", ex) + } + + if (foundFile == null) { + logger.info("Could not find symbols archive at path: $this}") + } + + return foundFile + } + + private fun getUnitySymbolsDirectory(projectDirectory: Directory, subDirName: String): File? { + return try { + val dir = projectDirectory.asFile.parentFile ?: return null + val symbolsDir = Paths.get( + dir.path, + subDirName, + UNITY_SYMBOLS_NAME + ).toFile() + + if (symbolsDir.exists()) { + symbolsDir + } else { + logger.info("Could not find Unity symbols directory at path ${symbolsDir.path}") + null + } + } catch (e: UnsupportedOperationException) { + logger.warn("Unexpected error searching for Unity symbols directory.", e) + null + } + } + + private fun extractSoFilesFromZipFile( + objFolder: File, + buildDir: Directory, + variantData: AndroidCompactedVariantData + ): Array { + val decompressedFile = File(getUncompressedUnityFilesPath(buildDir, variantData)) + + // Remove previous files from build intermediates folder + try { + if (decompressedFile.exists()) { + decompressedFile.listFiles()?.forEach { archDir -> + archDir.listFiles()?.forEach { arch -> + logger.info("Deleting existing symbol file: ${arch.path}") + Files.delete(arch.toPath()) + } + } + } + + decompressedFile.mkdirs() + } catch (ex: IOException) { + logger.warn( + "Failed to delete previous Unity symbol files from build intermediates directory: ${decompressedFile.absolutePath}", + ex + ) + } + + val buffer = ByteArray(BUFFER_SIZE) + val outDir: Path = Paths.get(getUncompressedUnityFilesPath(buildDir, variantData)) + + return try { + FileInputStream(objFolder).use { fis -> + BufferedInputStream(fis).use { bis -> + ZipInputStream(bis).use { stream -> + processZipStream(stream, outDir, buffer, decompressedFile).also { + logger.info("Using symbols extracted from archive at path: ${objFolder.path}") + } + } + } + } + } catch (ex: IOException) { + logger.error( + "Failed to decompress Unity symbols for file in path ${objFolder.path}.", + ex + ) + emptyArray() + } + } + + private fun processZipStream( + stream: ZipInputStream, + outDir: Path, + buffer: ByteArray, + decompressedFile: File + ): Array { + var entry = stream.nextEntry + while (entry != null) { + val filePath = outDir.resolve(entry.name) + + // This is true for Unity 2018 and 2019 which is no longer supported. + // Can remove after automation put in place that it's not used in some other code path for newer, supported Unity versions. + if (!entry.isDirectory) { + val name = entry.name + if (name.endsWith(UNITY_SYMBOL_EXTENSION_2019) || name.endsWith(UNITY_SYMBOL_IL2CPP_EXTENSION_2018)) { + logger.info("Extracting symbols from a non-directory archive with the name $name") + FileOutputStream(filePath.toFile()).use { fos -> + BufferedOutputStream(fos, buffer.size).use { bos -> + var len: Int + while (stream.read(buffer).also { len = it } > 0) { + bos.write(buffer, 0, len) + } + } + } + } + } else { + filePath.toFile().mkdirs() + } + entry = stream.nextEntry + } + return decompressedFile.listFiles() ?: emptyArray() + } + + private fun getUncompressedUnityFilesPath( + buildDir: Directory, + variantData: AndroidCompactedVariantData + ): String { + return "${buildDir.asFile.absoluteFile}/intermediates/embrace/unity/${getMappingFileFolder(variantData)}" + } + + private fun getMappingFileFolder(variantData: AndroidCompactedVariantData): String { + return if (variantData.flavorName.isBlank()) { + variantData.buildTypeName + } else { + "${variantData.flavorName}/${variantData.buildTypeName}" + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkSymbolsResource.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkSymbolsResource.kt new file mode 100644 index 0000000000..52173350b3 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkSymbolsResource.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class NdkSymbolsResource( + val symbols: Map> +) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkType.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkType.kt new file mode 100644 index 0000000000..25a4ea1006 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkType.kt @@ -0,0 +1,10 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +/** + * The type of NDK symbols. + */ +enum class NdkType { + NATIVE, + UNITY, + UNDEFINED +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshake.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshake.kt new file mode 100644 index 0000000000..050dc2a971 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshake.kt @@ -0,0 +1,81 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.network.HttpCallResult +import io.embrace.android.gradle.plugin.network.NetworkService + +/** + * This class is responsible for performing the NDK upload handshake. + * Given a list of symbols found during the build, it will return a list of symbols that should be uploaded. + * + * Example request. SymbolsOnHandshakeRequest:{ + * "app": "xxxxx", + * "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + * "variant": "release", + * "archs": { + * "x86_64": { + * "libnative.so": "f067beecae4f901c81c642d002810944460efd7b" + * }, + * "x86": { + * "libnative.so": "3c84ca4cf150a346db8d195426e520b7a45a0118" + * }, + * "armeabi-v7a": { + * "libnative.so": "b621b4bac764b4a1d6166984d63d9958187439a6" + * }, + * "arm64-v8a": { + * "libnative.so": "7d8c51cd16d00a369a1b923e1e9aed88c501beee" + * } + * } + * } + * + * Example response. SymbolsOnHandshakeResponse:{ + * "archs": { + * "arm64-v8a": [ + * "libnative.so" + * ], + * "armeabi-v7a": [ + * "libnative.so" + * ], + * "x86": [ + * "libnative.so" + * ], + * "x86_64": [ + * "libnative.so" + * ] + * } + * } + * When no symbols are requested, the service will return: + * { + * "archs": {} + * } + * and getRequestedSymbols will return null. + */ +class NdkUploadHandshake( + private val networkService: NetworkService +) { + private val logger = Logger(NdkUploadHandshake::class.java) + + fun getRequestedSymbols(request: NdkUploadHandshakeRequest): Map>? { + try { + val uploadResult = networkService.postNdkHandshake( + appId = request.appId, + handshake = request, + ) + if (uploadResult is HttpCallResult.Success<*>) { + val response = uploadResult.body as? NdkUploadHandshakeResponse ?: return null + val symbolsToUpload = response.symbols + return if (symbolsToUpload.isNullOrEmpty()) { + logger.info("No NDK files requested. Skipping NDK symbols upload.") + null + } else { + logger.info("Requested NDK symbols: " + response.symbols) + symbolsToUpload + } + } + return null + } catch (ex: Exception) { + logger.error("Failed to perform NDK Handshake. ", ex) + return null + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeRequest.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeRequest.kt new file mode 100644 index 0000000000..522ef9fa0b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeRequest.kt @@ -0,0 +1,23 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class NdkUploadHandshakeRequest( + @Json(name = "app") + val appId: String, + @Json(name = "token") + val apiToken: String, + @Json(name = "variant") + val variant: String?, + @Json(name = "archs") + val archSymbols: Map> +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeResponse.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeResponse.kt new file mode 100644 index 0000000000..cc7bfb57d2 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadHandshakeResponse.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.io.Serializable + +@JsonClass(generateAdapter = true) +data class NdkUploadHandshakeResponse( + @Json(name = "archs") + val symbols: Map>? +) : Serializable { + + private companion object { + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTask.kt new file mode 100644 index 0000000000..5791b43661 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTask.kt @@ -0,0 +1,353 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.config.UnitySymbolsDir +import io.embrace.android.gradle.plugin.hash.calculateSha1ForFile +import io.embrace.android.gradle.plugin.network.OkHttpNetworkService +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTask +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTaskImpl +import io.embrace.android.gradle.plugin.tasks.il2cpp.UnitySymbolFilesManager +import io.embrace.android.gradle.plugin.util.compression.ZstdFileCompressor +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.UnsupportedEncodingException +import javax.inject.Inject +import javax.xml.parsers.ParserConfigurationException +import javax.xml.transform.TransformerException + +/** + * A task that uploads NDK symbols to Embrace. + */ +abstract class NdkUploadTask @Inject constructor( + objectFactory: ObjectFactory +) : EmbraceUploadTask, EmbraceUploadTaskImpl(objectFactory) { + + private val logger = Logger(NdkUploadTask::class.java) + + private val deobfuscatedHashedObjects: MutableMap> = HashMap() + + private val unitySymbolFilesManager = UnitySymbolFilesManager.of() + + @get:Optional + @get:Input + val ndkType: Property = + objectFactory.property(NdkType::class.java).convention(NdkType.UNDEFINED) + + @get:Internal + val deobfuscatedFilesDirPath: DirectoryProperty = objectFactory.directoryProperty() + + @get:Input + @get:Optional + val unitySymbolsDir: Property = objectFactory.property( + UnitySymbolsDir::class.java + ) + + @get:Input + val ndkEnabled: Property = objectFactory.property(Boolean::class.java) + + @get:OutputDirectory + val generatedEmbraceResourcesDirectory: DirectoryProperty = objectFactory.directoryProperty() + + @get:InputFiles + @get:SkipWhenEmpty + val architecturesDirectoryForNative: ConfigurableFileCollection = objectFactory.fileCollection() + + @Inject + open fun getProjectLayout(): ProjectLayout { + throw UnsupportedOperationException() + } + + @TaskAction + fun onRun() { + if (!ndkEnabled.get()) { + logger.warn("NDK upload task will not run when the NDK is disabled.") + return + } + + if (ndkType.get() == NdkType.UNDEFINED) { + logger.warn("Cannot run NDK upload task without defining an NDK type.") + return + } + + try { + generateHashedObjects() + uploadHandshake() + injectSymbolsAsResources() + } catch (ex: Exception) { + val msg = "Failed uploading mapping artifact." + logger.error(msg) + throw IllegalStateException(msg, ex) + } + } + + @Throws(IllegalArgumentException::class) + private fun generateHashedObjects() { + // retrieve .so files by architecture + getSoFilesByArchitecture().forEach { (arch, sharedObjects) -> + val hashedObjects = mutableMapOf() + sharedObjects.forEach { sharedObject -> + try { + val outputDir = deobfuscatedFilesDirPath.dir(arch).get().asFile.absolutePath + val zstdFileCompressor = ZstdFileCompressor() + val compressedFile = zstdFileCompressor.compress( + sharedObject, + File(outputDir, sharedObject.name) + ) + if (compressedFile != null) { + // add hashed file by architecture + hashedObjects[compressedFile.name] = calculateSha1ForFile(compressedFile) + } + } catch (ex: Throwable) { + logger.error("Failed to generate hash for ${sharedObject.name} object.", ex) + } + } + if (hashedObjects.isNotEmpty()) { + deobfuscatedHashedObjects[arch] = hashedObjects + } else { + logger.error("Failed to generate hashed objects for any architectures") + } + } + } + + private fun getSoFilesByArchitecture(): Map> { + val archFiles = when (ndkType.get()) { + NdkType.UNITY -> { + getSoFilesByArchitectureForUnity() + } + + NdkType.NATIVE -> { + getSoFilesByArchitectureForNative() + } + + else -> { + throw IllegalArgumentException("Cannot generate NDK map file. Unsupported NDK type.") + } + } + + return generateArchSoMap(archFiles) + } + + private fun getSoFilesByArchitectureForUnity(): Array { + val symbolsDir = unitySymbolsDir.orNull + val unitySymbolFiles = if (this.unitySymbolsDir.isPresent && symbolsDir != null) { + unitySymbolFilesManager.getSymbolFiles( + symbolsDir, + getProjectLayout().buildDirectory.get(), + variantData.get() + ) + } else { + emptyArray() + } + + if (unitySymbolFiles.isEmpty()) { + logger.error("Unity symbol files not found") + } + + return unitySymbolFiles + } + + private fun getSoFilesByArchitectureForNative(): Array { + val files = if (!architecturesDirectoryForNative.isEmpty) { + architecturesDirectoryForNative.singleFile.listFiles() ?: emptyArray() + } else { + emptyArray() + } + + if (files.isEmpty()) { + logger.error("No mapping files found for native NDK") + } + + return files + } + + /** + * Generates a map of .so files based on architectures. + * + * @param listOfArch list of architectures files to process + */ + private fun generateArchSoMap(listOfArch: Array): HashMap> { + val archSoMap = HashMap>() + + listOfArch + .filter { it.exists() } + .forEach { arch -> + arch.listFiles { _, name -> name.endsWith(".so") }?.toList()?.let { soFileList -> + archSoMap[arch.name] = soFileList + soFileList.forEach { + logger.info("Symbol file found for arch ${arch.name} at path ${it.path}") + } + } + } + + return archSoMap + } + + /** + * Report the files we have available to see which ones the service does not have + */ + private fun uploadHandshake() { + val params = NdkUploadHandshakeRequest( + requestParams.get().appId, + requestParams.get().apiToken, + variantData.get().name, + deobfuscatedHashedObjects + ) + val networkService = OkHttpNetworkService(requestParams.get().baseUrl) + + NdkUploadHandshake(networkService).getRequestedSymbols(params)?.let { requestedSymbols -> + uploadSymbols(requestedSymbols) + } + } + + /** + * Attempt to upload requested symbols per architecture + */ + private fun uploadSymbols(requestedSymbols: Map>) { + filterRequestedSymbolsFiles(requestedSymbols).forEach { (arch, files) -> + files.forEach { (id: String, symbolFile: File) -> + try { + OkHttpNetworkService(requestParams.get().baseUrl).uploadNdkSymbolFile( + params = requestParams.get().copy(fileName = symbolFile.name), + file = symbolFile, + variantName = variantData.get().name, + arch = arch, + id = id, + ) + } catch (ex: Exception) { + logger.error( + "An exception occurred when attempting to upload symbols ${symbolFile.getName()} for arch $arch.", + ex + ) + } + } + } + } + + /** + * Filter the symbol files retrieved from build dir based on the required by service. + * + * @param requestedSymbols the required symbols + * @return a map of requested symbols grouped by architecture + */ + private fun filterRequestedSymbolsFiles( + requestedSymbols: Map> + ): Map> { + val selectedSymbols = mutableMapOf>() + getDeobfuscatedSymbolsFiles()?.let { deobfuscatedSymbols -> + requestedSymbols.forEach { (arch, symbols) -> + val requested = mutableMapOf() + deobfuscatedSymbols[arch]?.let { symbolFiles -> + symbolFiles.forEach { symbolFile -> + if (symbols.contains(symbolFile.name)) { + getSha1ByFile(symbolFile, arch)?.let { sha1 -> + requested[sha1] = symbolFile + } + } + } + if (requested.isNotEmpty()) { + selectedSymbols[arch] = requested + } + } + + if (requested.isEmpty()) { + logger.warn("None of the requested symbols for architecture $arch were found") + } + } + } + + if (selectedSymbols.isEmpty()) { + logger.error("None of the requested symbols were found") + } + + return selectedSymbols + } + + /** + * Get sha1 hash for the provided symbol file. + * + * @return sha1 hash + */ + private fun getSha1ByFile(symbolFile: File, archName: String): String? { + val sha1Value = deobfuscatedHashedObjects[archName]?.get(symbolFile.name) + if (sha1Value != null) { + logger.info("SHA1 found for architecture $archName for at ${symbolFile.path}: $sha1Value") + } else { + logger.info("SHA1 not found for architecture $archName for at ${symbolFile.path}") + } + + return sha1Value + } + + /** + * Retrieve symbols files by architecture from build dir. + * + * @return symbols files + */ + private fun getDeobfuscatedSymbolsFiles(): Map>? { + val symbolsDir = File(deobfuscatedFilesDirPath.asFile.get().absolutePath) + + if (!symbolsDir.exists()) { + logger.error("Deobfuscation directory does not exist, will return an empty map") + return null + } + + val archSoMap = mutableMapOf>() + symbolsDir.listFiles() + ?.filter { it.exists() } + ?.forEach { arch: File -> + val archName = arch.name + val soFiles = arch.listFiles() + if (soFiles != null) { + archSoMap[archName] = soFiles.toList() + } else { + logger.warn("No symbol files for architecture=$archName") + } + } + + if (archSoMap.isEmpty()) { + logger.error("No symbol files are found") + } + + return archSoMap + } + + @Throws( + FileNotFoundException::class, + UnsupportedEncodingException::class, + TransformerException::class, + ParserConfigurationException::class + ) + private fun injectSymbolsAsResources() { + val ndkSymbolsFile = generatedEmbraceResourcesDirectory.dir("values").get().asFile + + try { + ndkSymbolsFile.delete() + } catch (e: IOException) { + logger.info( + "Failed to delete previous config file for variant=${variantData.get().name} and path=${ndkSymbolsFile.path}" + ) + } + + val buildInfoFile = File(ndkSymbolsFile, FILE_NDK_SYMBOLS) + val injector = SymbolResourceInjector() + injector.writeSymbolResourceFile(buildInfoFile, deobfuscatedHashedObjects) + } + + companion object { + const val NAME: String = "ndkUploadTask" + const val FILE_NDK_SYMBOLS: String = "ndk_symbols.xml" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTaskRegistration.kt new file mode 100644 index 0000000000..a5aa8a7692 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/NdkUploadTaskRegistration.kt @@ -0,0 +1,184 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.config.ProjectType +import io.embrace.android.gradle.plugin.gradle.isTaskRegistered +import io.embrace.android.gradle.plugin.gradle.nullSafeMap +import io.embrace.android.gradle.plugin.gradle.registerTask +import io.embrace.android.gradle.plugin.gradle.safeFlatMap +import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.registration.EmbraceTaskRegistration +import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams +import io.embrace.android.gradle.plugin.util.capitalizedString +import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import java.util.concurrent.Callable + +private const val GENERATED_RESOURCE_PATH = "generated/embrace/res" + +/** + * In charge of registering tasks for ndk mapping file upload. + */ +class NdkUploadTaskRegistration( + private val behavior: PluginBehavior, +) : EmbraceTaskRegistration { + + override fun register(params: RegistrationParams) { + params.execute() + } + + /** + * Given the build variant and the ndk type, attempts to register the NDK upload task into the build variant's + * build process. + */ + fun RegistrationParams.execute(): TaskProvider? { + val variantExtension = extension.variants.getByName(data.name) + if (!variantExtension.ndkEnabled.get()) { + return null + } + + val mergeNativeLibsProvider = project.provider { + project.tryGetTaskProvider( + "merge${variant.name.capitalizedString()}NativeLibs" + ) + }.flatMap { it as Provider } + + val ndkUploadTaskProvider = project.registerTask( + NdkUploadTask.NAME, + NdkUploadTask::class.java, + data + ) { task -> + extension.variants.getByName(data.name).let { variantExtension -> + task.requestParams.set( + project.provider { + RequestParams( + appId = variantExtension.appId.get(), + apiToken = variantExtension.apiToken.get(), + endpoint = EmbraceEndpoint.NDK, + baseUrl = baseUrl, + ) + } + ) + + task.generatedEmbraceResourcesDirectory.set( + project.layout.buildDirectory.dir( + "$GENERATED_RESOURCE_PATH/${data.name}/ndk" + ) + ) + } + task.unitySymbolsDir.set( + variantExtension.projectType.nullSafeMap { + when (it) { + ProjectType.UNITY -> variantExtension.unitySymbolsDir.orNull + else -> null + } + } + ) + task.ndkEnabled.set(variantExtension.ndkEnabled) + task.deobfuscatedFilesDirPath.set( + project.layout.buildDirectory.dir( + "outputs/embrace/native/mapping/${getMappingFileFolder(data.buildTypeName, data.flavorName)}" + ) + ) + + val customSymbolsDirectory = behavior.customSymbolsDirectory + if (customSymbolsDirectory.isNullOrEmpty()) { + task.architecturesDirectoryForNative.from( + mergeNativeLibsProvider.safeFlatMap { libTask -> + // we want the directory where all architectures live. So to make it generic and + // work with any native build tool, we will go to any .so file, and then go up from + // there to the architectures directory. This way we don't have to hardcode any path + // For example: + // /Users/.../build/intermediates/embrace/NdkTest/_tmp_/app/build/ + // intermediates/merged_native_libs/release/out/lib/armeabi-v7a/libembrace-native.so + // the .so file will always be inside its corresponding architecture folder. + // By going 2 levels up, we will get all available architecture folders + return@safeFlatMap libTask.outputs.files.asFileTree.elements.nullSafeMap { + setOfNotNull( + it.firstOrNull { possibleSoFile -> + possibleSoFile.asFile.absolutePath.endsWith(".so") + }?.asFile?.parentFile?.parentFile + ) + } + } + ) + } else { + // if automatic detection works fine we should remove the usage of customSymbolsDirectory. It is + // still used in case there is a bug in new automatic symbols detection + val customSymbolsFile = project.rootProject.file(customSymbolsDirectory) + if (customSymbolsFile.exists()) { + task.architecturesDirectoryForNative.from(customSymbolsFile) + } else { + val msg = "Can not retrieve native symbols. Custom symbols " + + "directory=${customSymbolsFile.path} does not exist.\nMake sure native symbols are " + + "located in that directory" + error(msg) + } + } + } + + val taskContainer = project.tasks + ndkUploadTaskProvider.configure { ndkUploadTask: NdkUploadTask -> + + val ndkEnabled = variantExtension.ndkEnabled + ndkUploadTask.onlyIf { ndkEnabled.getOrElse(false) } + ndkUploadTask.ndkType.set( + variantExtension.projectType.map { + when (it) { + ProjectType.UNITY -> NdkType.UNITY + ProjectType.NATIVE -> { + if (behavior.customSymbolsDirectory.isNullOrEmpty() || + taskContainer.isTaskRegistered( + "externalNativeBuild", + variantExtension.name + ) + ) { + NdkType.NATIVE + } else { + NdkType.UNDEFINED + } + } + + else -> { + NdkType.UNDEFINED + } + } + } + ) + ndkUploadTask.mustRunAfter(object : Callable { + override fun call(): Any { + return listOfNotNull( + project.tryGetTaskProvider( + "merge${variantExtension.name.capitalizedString()}JniLibFolders" + ), + project.tryGetTaskProvider( + "transformNativeLibsWithMergeJniLibsFor${variantExtension.name.capitalizedString()}" + ), + project.tryGetTaskProvider( + "merge${variantExtension.name.capitalizedString()}NativeLibs" + ) + ) + } + }) + } + ndkUploadTaskProvider.let { + variant.sources.res?.addGeneratedSourceDirectory( + it, + NdkUploadTask::generatedEmbraceResourcesDirectory + ) + } + return ndkUploadTaskProvider + } + + private fun getMappingFileFolder( + buildTypeName: String?, + flavorName: String? + ) = if (flavorName.isNullOrEmpty()) { + buildTypeName ?: "" + } else { + "$flavorName/$buildTypeName" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/SymbolResourceInjector.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/SymbolResourceInjector.kt new file mode 100644 index 0000000000..0a4603d3d8 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/ndk/SymbolResourceInjector.kt @@ -0,0 +1,74 @@ +package io.embrace.android.gradle.plugin.tasks.ndk + +import io.embrace.android.gradle.plugin.tasks.BuildResourceWriter +import io.embrace.android.gradle.plugin.util.serialization.EmbraceSerializer +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import java.io.File +import java.nio.charset.StandardCharsets +import java.util.Base64 + +/** + * Injects information about NDK symbol resources into the APK. + */ +class SymbolResourceInjector(private val serializer: EmbraceSerializer = MoshiSerializer()) { + + /** + * Writes symbol information into a build file that is incorporated into the APK's resources. + */ + fun writeSymbolResourceFile( + dst: File, + hashedSymbols: Map> + ) { + val value = buildSymbolResourceValue(hashedSymbols) ?: return + BuildResourceWriter().writeBuildInfoFile(dst, mapOf(KEY_NDK_SYMBOLS to value)) + } + + private fun buildSymbolResourceValue(deobfuscatedHashedObjects: Map>): String? { + val symbolsResource = createNdkSymbolsResource(deobfuscatedHashedObjects) + val symbols = try { + serializer.toJson(symbolsResource) + } catch (e: Exception) { + null + } + + if (!symbols.isNullOrEmpty()) { + val bytes = symbols.toByteArray(StandardCharsets.UTF_8) + val encoder = Base64.getEncoder() + val encodedSymbols = String(encoder.encode(bytes)) + return encodedSymbols + } + return null + } + + private fun createNdkSymbolsResource(deobfuscatedHashedObjects: Map>): NdkSymbolsResource { + val unityFilenameMapping = mapOf( + "libunity.sym.so" to "libunity.so", + "libil2cpp.sym" to "libil2cpp.so", + "libil2cpp.sym.so" to "libil2cpp.so" + ) + + /* + * Unity symbol file names and stacktrace names do not match. So that we can match up + * a stack trace line with the right symbol file, we remap the resource names we inject + * to match the names that will be present in stacktraces. + */ + val resourceSymbols = mutableMapOf>() + + deobfuscatedHashedObjects.forEach { (arch, archSymbols) -> + if (archSymbols.isEmpty()) { + return@forEach + } + val archObjects = mutableMapOf() + archSymbols.forEach { (name, sha) -> + val finalName = unityFilenameMapping[name] ?: name + archObjects[finalName] = sha + } + resourceSymbols[arch] = archObjects + } + return NdkSymbolsResource(resourceSymbols) + } + + companion object { + private const val KEY_NDK_SYMBOLS = "emb_ndk_symbols" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/r8/JvmMappingUploadTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/r8/JvmMappingUploadTaskRegistration.kt new file mode 100644 index 0000000000..88131d79c8 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/r8/JvmMappingUploadTaskRegistration.kt @@ -0,0 +1,156 @@ +package io.embrace.android.gradle.plugin.tasks.r8 + +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.Variant +import io.embrace.android.gradle.plugin.agp.AgpUtils.isDexguard +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.gradle.isTaskRegistered +import io.embrace.android.gradle.plugin.gradle.nullSafeMap +import io.embrace.android.gradle.plugin.gradle.registerTask +import io.embrace.android.gradle.plugin.gradle.safeFlatMap +import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.common.FileCompressionTask +import io.embrace.android.gradle.plugin.tasks.common.MultipartUploadTask +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.registration.EmbraceTaskRegistration +import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams +import io.embrace.android.gradle.plugin.util.capitalizedString +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import java.io.File + +class JvmMappingUploadTaskRegistration : EmbraceTaskRegistration { + + companion object { + private const val COMPRESS_TASK_NAME = "jvmMappingCompressionTaskFor" + private const val UPLOAD_TASK_NAME = "jvmMappingUploadTaskFor" + private const val FILE_NAME_MAPPING_TXT = "mapping.txt" + } + + override fun register(params: RegistrationParams) { + with(params) { + project.afterEvaluate { + val tasks = fetchJvmMappingTasks(project, data) + tasks.forEach { task -> + register( + project, + data, + task, + fetchJvmMappingFile(task, variant), + baseUrl, + extension + ) + } + } + } + } + + private fun register( + project: Project, + variant: AndroidCompactedVariantData, + anchorTask: TaskProvider, + mappingFile: Provider, + baseUrl: String, + extension: EmbraceExtensionInternal, + ): TaskProvider { + val compressionTask = project.registerTask( + "$COMPRESS_TASK_NAME${variant.name}", + FileCompressionTask::class.java, + variant + ) { task: FileCompressionTask -> + // automatically link as a task dependency + task.originalFile.fileProvider(mappingFile) + task.compressedFile.convention( + project.layout.buildDirectory.file( + "outputs/embrace/mapping/compressed/${variant.name}/$FILE_NAME_MAPPING_TXT" + ) + ) + } + + val uploadTask = project.registerTask( + "$UPLOAD_TASK_NAME${variant.name}", + MultipartUploadTask::class.java, + variant + ) { task -> + val variantExtension = extension.variants.getByName(variant.name) + task.requestParams.set( + project.provider { + RequestParams( + appId = variantExtension.appId.get(), + apiToken = variantExtension.apiToken.get(), + endpoint = EmbraceEndpoint.PROGUARD, + fileName = FILE_NAME_MAPPING_TXT, + buildId = variantExtension.buildId.get(), + baseUrl = baseUrl, + ) + } + ) + + // link output of compression task to the input of this task + // dependencies to mapping file compression will be added automatically + task.uploadFile.fileProvider( + compressionTask.nullSafeMap { + it.compressedFile.orNull?.asFile + } + ) + } + + // set us (Embrace) as a dependency of the obfuscation task + anchorTask.configure { task -> + task.finalizedBy(uploadTask) + } + return uploadTask + } + + private fun fetchJvmMappingFile( + obfuscationTask: TaskProvider, + variant: Variant + ): Provider { + return if (isDexguard(obfuscationTask)) { + fetchDexguardMappingFile(obfuscationTask) + } else { + variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE) + .map { it.asFile } + } + } + + private fun fetchDexguardMappingFile( + obfuscationTask: TaskProvider + ): Provider { + return obfuscationTask.safeFlatMap { task -> + task.outputs.files.asFileTree.filter { + it.name == "mapping.txt" + }.elements.nullSafeMap { + it.firstOrNull()?.asFile + } + } + } + + /** + * It fetches all available obfuscation tasks. In practice, a single task will be + * executed but multiple tasks may be registered and configured. Because of this, we are + * forced to return a list of tasks since we are in configuration phase. + */ + private fun fetchJvmMappingTasks( + project: Project, + variant: AndroidCompactedVariantData + ): List> { + val name = variant.name.capitalizedString() + val targetObfuscationTasks = listOf( + "dexguardApk$name", + "dexguardAab$name", + "transformClassesAndResourcesWithProguardFor$name", + "minify${name}WithProguard", + "transformClassesAndResourcesWithR8For$name", + "minify${name}WithR8" + ) + val tasks = targetObfuscationTasks.filter { taskName -> + isTaskRegistered(project.tryGetTaskProvider(taskName)) + } + return tasks.mapNotNull { project.tryGetTaskProvider(it) } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTask.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTask.kt new file mode 100644 index 0000000000..3a1e76fadc --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTask.kt @@ -0,0 +1,157 @@ +package io.embrace.android.gradle.plugin.tasks.reactnative + +import com.squareup.moshi.JsonWriter +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.hash.calculateMD5ForFile +import io.embrace.android.gradle.plugin.network.OkHttpNetworkService +import io.embrace.android.gradle.plugin.tasks.BuildResourceWriter +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTask +import io.embrace.android.gradle.plugin.tasks.EmbraceUploadTaskImpl +import okio.buffer +import okio.sink +import okio.source +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.GZIPOutputStream +import javax.inject.Inject + +/** + * Task to upload React Native sourcemap artefacts to Embrace. + */ +abstract class EmbraceRnSourcemapGeneratorTask @Inject constructor( + objectFactory: ObjectFactory +) : EmbraceUploadTask, EmbraceUploadTaskImpl(objectFactory) { + + private val logger = Logger(EmbraceRnSourcemapGeneratorTask::class.java) + + @get:OutputDirectory + val generatedEmbraceResourcesDirectory: DirectoryProperty = objectFactory.directoryProperty() + + @get:Optional + @get:InputFile + val sourcemap: RegularFileProperty = objectFactory.fileProperty() + + @get:Optional + @get:InputFile + val bundleFile: RegularFileProperty = objectFactory.fileProperty() + + @get:OutputFile + val sourcemapAndBundleFile: RegularFileProperty = objectFactory.fileProperty() + + @get:Input + val reactProperties: MapProperty = objectFactory.mapProperty( + String::class.java, + Any::class.java + ).convention(emptyMap()) + + @TaskAction + fun onRun() { + val rnFilesFinderUtil = RnFilesFinder( + reactProperties.get(), + project.layout.buildDirectory.get().asFile + ) + + val bundleFile = rnFilesFinderUtil.fetchJSBundleFile( + bundleFile.orNull?.asFile + ) + if (bundleFile == null) { + logger.error("Couldn't find the JSBundle. React native files were not uploaded.") + return + } + + val bundleId = calculateMD5ForFile(bundleFile) + + /** + * In old React Native Versions, the source map is not exposed as output in the task. + * If the source map is not present, we will search for it in the known location + */ + val sourceMapFile: File? = rnFilesFinderUtil.fetchSourceMapFile( + sourcemap.orNull?.asFile, + variantData.get() + ) + + if (sourceMapFile == null || !sourceMapFile.exists()) { + logger.error("Couldn't find the Source Map. React native files were not uploaded.") + return + } + + val sourceMapAndBundleJsonFile = sourcemapAndBundleFile.asFile.get() + + uploadBundleFiles( + generateBundleZipFile( + bundleFile, + sourceMapFile, + sourceMapAndBundleJsonFile + ), + bundleId + ) + + injectSymbolsAsResources(bundleId) + } + + private fun injectSymbolsAsResources(bundleId: String) { + try { + val resValues = mapOf( + BUNDLE_INFO_APP_ID to bundleId + ) + val dir = File(generatedEmbraceResourcesDirectory.asFile.get(), "values") + BuildResourceWriter().writeBuildInfoFile( + File(dir, FILE_RN_INFO_XML), + resValues + ) + } catch (ex: IOException) { + throw IllegalStateException("The build info file generation failed.", ex) + } + } + + private fun uploadBundleFiles(jsonFile: File, bundleId: String) { + OkHttpNetworkService(requestParams.get().baseUrl).uploadRnSourcemapFile( + requestParams.get().copy(buildId = bundleId), + jsonFile + ) + } + + private fun generateBundleZipFile( + bundleFile: File, + sourceMapFile: File, + sourceMapAndBundleJsonFile: File + ): File { + try { + sourceMapAndBundleJsonFile.parentFile.mkdirs() + val fos = GZIPOutputStream(FileOutputStream(sourceMapAndBundleJsonFile)).sink().buffer() + JsonWriter.of(fos).use { jsonWriter -> + with(jsonWriter) { + beginObject() + name(KEY_NAME_BUNDLE) + bundleFile.source().buffer().use { value(it) } + name(KEY_NAME_SOURCE_MAP) + sourceMapFile.source().buffer().use { value(it) } + endObject() + } + } + } catch (e: Exception) { + val msg = "Failed to generate bundle zip file with bundleFile: $bundleFile and sourceMapFile: $sourceMapFile" + logger.error(msg) + throw IllegalStateException(msg, e) + } + return sourceMapAndBundleJsonFile + } + + companion object { + private const val KEY_NAME_BUNDLE = "bundle" + private const val KEY_NAME_SOURCE_MAP = "sourcemap" + private const val FILE_RN_INFO_XML: String = "rn_sourcemap.xml" + private const val BUNDLE_INFO_APP_ID = "emb_rn_bundle_id" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTaskRegistration.kt new file mode 100644 index 0000000000..b3891cb40b --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/EmbraceRnSourcemapGeneratorTaskRegistration.kt @@ -0,0 +1,169 @@ +package io.embrace.android.gradle.plugin.tasks.reactnative + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.gradle.nullSafeMap +import io.embrace.android.gradle.plugin.gradle.registerTask +import io.embrace.android.gradle.plugin.gradle.safeFlatMap +import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.registration.EmbraceTaskRegistration +import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams +import org.gradle.api.Task +import org.gradle.api.plugins.ExtraPropertiesExtension.UnknownPropertyException +import org.gradle.api.tasks.TaskProvider + +private const val SOURCEMAP_GENERATOR_NAME = "embraceRNSourcemapGeneratorFor" +private const val REACT_PROPERTY = "react" +private const val RN_BUNDLE_FILE_EXTENSION = ".bundle" +private const val RN_SOURCEMAP_FILE_EXTENSION = "bundle.map" +private const val RN_BUNDLE_KEY = ".bundle" +private const val RN_SOURCEMAP_KEY = "bundle.map" + +private const val GENERATED_RESOURCE_PATH = "generated/embrace/res" + +class EmbraceRnSourcemapGeneratorTaskRegistration : EmbraceTaskRegistration { + + private val logger = Logger(EmbraceRnSourcemapGeneratorTaskRegistration::class.java) + + override fun register(params: RegistrationParams) { + params.project.afterEvaluate { + params.execute() + } + } + + /** + * Given the build variant, attempts to register the RN Source Map generator task into the + * build variant's build + * process. + *

+ * We choose process${variant}JavaRes and compile${variant}JavaWithJavac tasks as anchor + * for the ReactNativeBundleRetrieverTask because it's a safe stage to assume that both, the + * bundle and the Source Map, have been already generated. + */ + private fun RegistrationParams.execute(): TaskProvider? { + // Prevent upload the bundle in debug variant + if (data.isBuildTypeDebuggable) { + return null + } + + val variantCapitalized = variant.name + .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + + // React Native started to use this task after version 0.71 + + val newRNSourceFilesGeneratorTask = "createBundle${variantCapitalized}JsAndAssets" + + // For React Native versions prior to 0.71, this task is used to generate and copy the + // source map and bundle files into the APK/AAB before packaging + val oldRNSourceFilesGeneratorTask = "bundle${variantCapitalized}JsAndAssets" + + val generatorTask = project.tryGetTaskProvider(newRNSourceFilesGeneratorTask) + ?: project.tryGetTaskProvider(oldRNSourceFilesGeneratorTask) + + if (generatorTask == null || !generatorTask.isPresent) { + logger.error( + "We could not find the task that generates the Bundle. Please submit the Bundle and Source Map manually." + + "https://embrace.io/docs/react-native/integration/upload-symbol-files/?platform=android#symbolication-with-codepush" + ) + return null + } + + val taskProvider = createRnSourcemapGeneratorTaskProvider(generatorTask) + taskProvider.let { task -> + variant.sources.res?.addGeneratedSourceDirectory( + task, + EmbraceRnSourcemapGeneratorTask::generatedEmbraceResourcesDirectory + ) + } + return taskProvider + } + + private fun RegistrationParams.createRnSourcemapGeneratorTaskProvider( + generatorTask: TaskProvider + ) = project.registerTask( + SOURCEMAP_GENERATOR_NAME, + EmbraceRnSourcemapGeneratorTask::class.java, + data + ) { rnTask -> + try { + val variantExtension = extension.variants.getByName(variant.name) + rnTask.requestParams.set( + project.provider { + RequestParams( + appId = variantExtension.appId.get(), + apiToken = variantExtension.apiToken.get(), + endpoint = EmbraceEndpoint.SOURCE_MAP, + fileName = FILE_NAME_SOURCE_MAP_JSON, + baseUrl = baseUrl, + ) + } + ) + + rnTask.generatedEmbraceResourcesDirectory.set( + project.layout.buildDirectory.dir( + "${GENERATED_RESOURCE_PATH}/${data.name}/react_native" + ) + ) + + val bundleAndSourceMapFilesProvider = generatorTask.safeFlatMap { task -> + return@safeFlatMap task.outputs.files.asFileTree.elements.nullSafeMap { files -> + return@nullSafeMap files.filter { possibleSoFile -> + possibleSoFile.asFile.absolutePath.endsWith( + RN_BUNDLE_FILE_EXTENSION + ) || + possibleSoFile.asFile.absolutePath.endsWith( + RN_SOURCEMAP_FILE_EXTENSION + ) + }.associate { fileSystemLocation -> + val file = fileSystemLocation.asFile + val key = when { + file.absolutePath.endsWith(RN_BUNDLE_FILE_EXTENSION) -> RN_BUNDLE_KEY + file.absolutePath.endsWith(RN_SOURCEMAP_FILE_EXTENSION) -> RN_SOURCEMAP_KEY + else -> "none" + } + key to project.layout.file(project.provider { file }).get() + } + } + } + + rnTask.bundleFile.set( + bundleAndSourceMapFilesProvider.nullSafeMap { + it[RN_BUNDLE_KEY] + } + ) + + rnTask.sourcemap.set( + bundleAndSourceMapFilesProvider.nullSafeMap { + it[RN_SOURCEMAP_KEY] + } + ) + + val reactProperties = try { + @Suppress("UNCHECKED_CAST") + project.extensions.extraProperties.get(REACT_PROPERTY) as? Map + } catch (e: UnknownPropertyException) { + null + } + rnTask.reactProperties.set(reactProperties) + + val flavorName = data.flavorName + val buildTypeName = data.buildTypeName + val bundleFileFolder = + if (flavorName.isBlank()) buildTypeName else "$flavorName/$buildTypeName" + rnTask.sourcemapAndBundleFile.set( + project.layout.buildDirectory.file( + "outputs/embrace/$bundleFileFolder/$FILE_NAME_SOURCE_MAP_JSON" + ) + ) + } catch (e: Exception) { + logger.error( + "EmbraceRNSourcemapGeneratorTask failed while getting the Bundle and the SourceMap", + e + ) + } + } + + private companion object { + private const val FILE_NAME_SOURCE_MAP_JSON = "sourcemap.json" + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/RnFilesFinder.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/RnFilesFinder.kt new file mode 100644 index 0000000000..bd544bdbf5 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/reactnative/RnFilesFinder.kt @@ -0,0 +1,137 @@ +package io.embrace.android.gradle.plugin.tasks.reactnative + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import java.io.File + +private const val SOURCE_MAP_NAME = "android-embrace.bundle.map" +private const val BUNDLE_ASSET_NAME = "bundleAssetName" +private const val DEFAULT_BUNDLE_NAME = "index.android.bundle" +private const val REACT_EXTRA_PACKAGER_ARGS = "extraPackagerArgs" +private const val SOURCEMAP_COMMAND_NAME = "--sourcemap-output" + +/** + * Class that contains the logic to find the React Native sourcemap. + */ + +class RnFilesFinder( + private val reactProps: Map?, + private val buildDirectory: File, + private val logger: Logger = Logger(RnFilesFinder::class.java) +) { + + /** + * Look for sourcemap file in generated/sourcemaps/react/variantDir + * + * If a custom sourcemap file name has been set in a gradle property, use it. + * + * If not, use the default name. + * + * @return React native source map file + */ + fun getReactNativeSourcemapFilePath(variantDirName: String): String { + val sourceMapsDirPath = "generated/sourcemaps/react/$variantDirName" + val bundleAssetName = getBundleAssetName(reactProps) + + return "$sourceMapsDirPath/$bundleAssetName.map" + } + + /** + * Look for android-embrace.bundle.map sourcemap file, located in generated/sourcemaps/ + * + * @return React native source map file + */ + fun getEmbraceSourcemapFilePath(): String { + logDeprecatedCommands() + val sourceMapsDirPath = "generated/sourcemaps" + return "$sourceMapsDirPath/$SOURCE_MAP_NAME" + } + + @Suppress("UNCHECKED_CAST") + private fun logDeprecatedCommands() { + val extraPackagerArgValues = reactProps?.get(REACT_EXTRA_PACKAGER_ARGS) as? List ?: return + if (extraPackagerArgValues.any { it.contains(SOURCEMAP_COMMAND_NAME) }) { + logger.warn( + "DEPRECATED: The command $SOURCEMAP_COMMAND_NAME will be deprecated. Please remove" + + " that from your app/build.gradle $REACT_EXTRA_PACKAGER_ARGS" + ) + } + } + + /** + * Iterate through all folders inside build folder to find index.android.bundle + */ + private fun getFirstBundleFileFoundedInBuildFolder(): File? { + return buildDirectory.walk() + .filter { it.isFile && (it.name == "index.android.bundle") } + .firstOrNull() + } + + /** + * Iterate through all folders inside build folder to find index.android.bundle + * + * @return React native bundle file + */ + fun getBundleFile(): File? { + return getFirstBundleFileFoundedInBuildFolder() + } + + private fun getBundleAssetName(reactProps: Map?) = + reactProps?.get(BUNDLE_ASSET_NAME)?.toString() ?: DEFAULT_BUNDLE_NAME + + /** + * Retrieves the source map file. If the provided source map file is already present, it returns that file. + * Otherwise, it searches for the source map file in specific build directories and returns the first one found. + * + * @param sourceMapInputFile the source map file property to check first. + * @param variant the Android variant data containing the necessary source map path information. + * @return the source map file if found, or `null` if no source map file could be located. + */ + fun fetchSourceMapFile( + sourceMapInputFile: File?, + variant: AndroidCompactedVariantData + ): File? { + if (sourceMapInputFile != null && sourceMapInputFile.exists()) { + return sourceMapInputFile + } + + // for older RN versions we might not locate sourcemap file in configuration phase + // this is likely due to a bug in RN plugin where they don't set sourcemap file as output + // of the task. In this case, we'll look in hardcoded paths + val embraceSourceMapFile = File(buildDirectory, getEmbraceSourcemapFilePath()) + val reactNativeSourceMapFile = File( + buildDirectory, + getReactNativeSourcemapFilePath( + variant.sourceMapPath + ) + ) + + return if (embraceSourceMapFile.exists()) { + embraceSourceMapFile + } else if (reactNativeSourceMapFile.exists()) { + reactNativeSourceMapFile + } else { + null + } + } + + /** + * Retrieves the JSBundle file. If the provided JSBundle file is already present, it returns that file. + * Otherwise, it searches for the JSBundle file in specific build directories and returns the first one found. + * + * @param bundleInputFile the source map file to check first. + * @return the bundle file if found, or `null` if no bundle file could be located. + */ + fun fetchJSBundleFile(bundleInputFile: File?): File? { + return if (bundleInputFile != null && bundleInputFile.exists()) { + bundleInputFile + } else { + logger.info( + "Couldn't get the JSBundle from its generator task, " + + "Will iterate through all folders inside build folder to find index.android.bundle" + ) + + return getBundleFile() + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/EmbraceTaskRegistration.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/EmbraceTaskRegistration.kt new file mode 100644 index 0000000000..3cb92b9309 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/EmbraceTaskRegistration.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.plugin.tasks.registration + +/** + * Implementations of this interface are responsible for registering tasks. + */ +interface EmbraceTaskRegistration { + + /** + * Register a task with the given parameters. It is the responsibility of the implementation + * to use project.afterEvaluate (if required). + */ + fun register(params: RegistrationParams) +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/RegistrationParams.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/RegistrationParams.kt new file mode 100644 index 0000000000..18d9b6d819 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/RegistrationParams.kt @@ -0,0 +1,16 @@ +package io.embrace.android.gradle.plugin.tasks.registration + +import com.android.build.api.variant.Variant +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.network.NetworkService +import org.gradle.api.Project + +class RegistrationParams( + val project: Project, + val variant: Variant, + val data: AndroidCompactedVariantData, + val networkService: NetworkService, + val extension: EmbraceExtensionInternal, + val baseUrl: String, +) diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/TaskRegistrar.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/TaskRegistrar.kt new file mode 100644 index 0000000000..2abed6f437 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/tasks/registration/TaskRegistrar.kt @@ -0,0 +1,98 @@ +package io.embrace.android.gradle.plugin.tasks.registration + +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant +import io.embrace.android.gradle.plugin.config.PluginBehavior +import io.embrace.android.gradle.plugin.config.variant.EmbraceVariantConfigurationBuilder +import io.embrace.android.gradle.plugin.dependency.installDependenciesForVariant +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternalSource +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.network.NetworkService +import io.embrace.android.gradle.plugin.tasks.il2cpp.Il2CppUploadTaskRegistration +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadTaskRegistration +import io.embrace.android.gradle.plugin.tasks.r8.JvmMappingUploadTaskRegistration +import io.embrace.android.gradle.plugin.tasks.reactnative.EmbraceRnSourcemapGeneratorTaskRegistration +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty + +/** + * Contains the logic for configuring tasks for each variant. + */ +class TaskRegistrar( + private val project: Project, + private val behavior: PluginBehavior, + private val embraceVariantConfigurationBuilder: EmbraceVariantConfigurationBuilder, + private val variantConfigurationsListProperty: ListProperty, + private val networkService: NetworkService, +) { + + private val embraceExtensionInternal: EmbraceExtensionInternal = checkNotNull( + project.extensions.findByType(EmbraceExtensionInternal::class.java) + ) + + /** + * It is in charge of looping through each variant and configure each task. + * Each subclass will have its proper looping and handling of variant, since this is unique to each approach. + */ + fun registerTasks() { + project.extensions.getByType(AndroidComponentsExtension::class.java) + .onVariants { variant: Variant -> + onVariant(AndroidCompactedVariantData.from(variant), variant) + } + } + + private fun onVariant(variant: AndroidCompactedVariantData, ref: Variant) { + project.installDependenciesForVariant(variant.name, behavior) + + EmbraceExtensionInternalSource().setupExtension( + project, + behavior, + variant, + embraceVariantConfigurationBuilder, + variantConfigurationsListProperty + ) + + if (behavior.isPluginDisabledForVariant(variant.name) || !shouldRegisterUploadTasks(variant)) { + return + } else { + val params = RegistrationParams( + project, + ref, + variant, + networkService, + embraceExtensionInternal, + behavior.baseUrl, + ) + registerTasks(params) + } + } + + private fun registerTasks(params: RegistrationParams) { + JvmMappingUploadTaskRegistration().register(params) + if (behavior.isReactNativeProject) { + val taskRegistration = EmbraceRnSourcemapGeneratorTaskRegistration() + taskRegistration.register(params) + } + NdkUploadTaskRegistration(behavior).register(params) + if (behavior.isIl2CppMappingFilesUploadEnabled) { + Il2CppUploadTaskRegistration().register(params) + } + } + + private fun shouldRegisterUploadTasks(variant: AndroidCompactedVariantData): Boolean { + return if (behavior.isUploadMappingFilesDisabled) { + false + } else { + val variantExtension = embraceExtensionInternal.variants.getByName(variant.name) + if (variantExtension.apiToken.orNull.isNullOrEmpty()) { + false + } else if (variantExtension.appId.orNull.isNullOrEmpty()) { + false + } else { + true + } + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/CapitalizedKt.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/CapitalizedKt.kt new file mode 100644 index 0000000000..5a029a9637 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/CapitalizedKt.kt @@ -0,0 +1,3 @@ +package io.embrace.android.gradle.plugin.util + +fun CharSequence.capitalizedString(): String = this.toString().replaceFirstChar { it.uppercase() } diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/UuidUtils.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/UuidUtils.kt new file mode 100644 index 0000000000..22fa754d89 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/UuidUtils.kt @@ -0,0 +1,24 @@ +package io.embrace.android.gradle.plugin.util + +import java.util.Locale +import java.util.UUID + +/** + * General utility functions for dealing with UUIDs. + * + * + * This class should be completely functional and should not contain any instance methods. + */ +object UuidUtils { + + /** + * Generates a random UUID using Java Utils library, and formats it into an Embrace API + * compatible build UUID. + * + * @return Embrace API compatible randomly generated UUID + */ + @JvmOverloads + fun generateEmbraceUuid(uuid: UUID = UUID.randomUUID()): String { + return uuid.toString().replace("[^a-zA-Z0-9]".toRegex(), "").uppercase(Locale.getDefault()) + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/FileCompressor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/FileCompressor.kt new file mode 100644 index 0000000000..fd19596bf9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/FileCompressor.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.plugin.util.compression + +import java.io.File + +interface FileCompressor { + + fun compress(inputFile: File, outputFile: File): File? +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressor.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressor.kt new file mode 100644 index 0000000000..696043367d --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressor.kt @@ -0,0 +1,36 @@ +package io.embrace.android.gradle.plugin.util.compression + +import com.github.luben.zstd.ZstdOutputStream +import io.embrace.android.gradle.plugin.Logger +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + +class ZstdFileCompressor : FileCompressor { + private val logger = Logger(ZstdFileCompressor::class.java) + + override fun compress(inputFile: File, outputFile: File): File? { + return try { + val parent = outputFile.parentFile ?: return null + if (!parent.exists()) { + parent.mkdirs() + } + ZstdOutputStream(FileOutputStream(outputFile)).use { outStream -> + FileInputStream(inputFile).use { stdout -> + val buffer = ByteArray(BUFFER_SIZE) + var n: Int + while (stdout.read(buffer).also { n = it } > -1) { + outStream.write(buffer, 0, n) + } + return outputFile + } + } + } catch (e: IOException) { + logger.error("Error compressing ${inputFile.path} to ${outputFile.path}", e) + null + } + } +} + +private const val BUFFER_SIZE = 16384 diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/EmbraceSerializer.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/EmbraceSerializer.kt new file mode 100644 index 0000000000..e64dc177db --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/EmbraceSerializer.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.plugin.util.serialization + +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.Type + +interface EmbraceSerializer { + fun toJson(data: T): String + fun toJson(data: T, type: Type): String + fun toJson(any: T, clazz: Class, outputStream: OutputStream) + fun fromJson(json: String, type: Class): T + fun fromJson(inputStream: InputStream, clz: Class): T +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializer.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializer.kt new file mode 100644 index 0000000000..230c8d03f9 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializer.kt @@ -0,0 +1,69 @@ +package io.embrace.android.gradle.plugin.util.serialization + +import com.squareup.moshi.Moshi +import okio.buffer +import okio.sink +import okio.source +import java.io.InputStream +import java.io.OutputStream +import java.lang.reflect.Type + +class MoshiSerializer : EmbraceSerializer { + + private val moshiRef = object : ThreadLocal() { + override fun initialValue(): Moshi = Moshi.Builder() + .build() + } + + private val moshiImpl by lazy { checkNotNull(moshiRef.get()) } + + override fun toJson(data: T): String { + return try { + val javaClass = checkNotNull(data)::class.java + val adapter = moshiImpl.adapter(javaClass) + adapter.toJson(data) + } catch (e: Exception) { + throw IllegalArgumentException("Failed to serialize object: ${e.message}", e) + } + } + + override fun toJson(data: T, type: Type): String { + return try { + val adapter = moshiImpl.adapter(type) + adapter.toJson(data) + } catch (e: Exception) { + throw IllegalArgumentException("Failed to serialize object: ${e.message}", e) + } + } + + override fun toJson(any: T, clazz: Class, outputStream: OutputStream) { + return try { + outputStream.sink().buffer().use { + val adapter = moshiImpl.adapter(clazz) + adapter.toJson(it, any) + } + } catch (e: Exception) { + throw IllegalArgumentException("Failed to serialize object: ${e.message}", e) + } + } + + override fun fromJson(json: String, type: Class): T { + return try { + val adapter = moshiImpl.adapter(type) + adapter.fromJson(json) ?: throw IllegalArgumentException("Failed to deserialize object") + } catch (e: Exception) { + throw IllegalArgumentException("Failed to deserialize object: ${e.message}", e) + } + } + + override fun fromJson(inputStream: InputStream, clz: Class): T { + return try { + inputStream.source().buffer().use { + val adapter = moshiImpl.adapter(clz) + adapter.fromJson(it) ?: error("JSON conversion failed.") + } + } catch (e: Exception) { + throw IllegalArgumentException("Failed to deserialize object: ${e.message}", e) + } + } +} diff --git a/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/swazzler/plugin/extension/SwazzlerExtension.kt b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/swazzler/plugin/extension/SwazzlerExtension.kt new file mode 100644 index 0000000000..84aad77504 --- /dev/null +++ b/embrace-gradle-plugin/src/main/java/io/embrace/android/gradle/swazzler/plugin/extension/SwazzlerExtension.kt @@ -0,0 +1,90 @@ +package io.embrace.android.gradle.swazzler.plugin.extension + +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property + +/** + * An extension that can be used to configure the plugin. + */ +abstract class SwazzlerExtension(objectFactory: ObjectFactory) { + + val forceIncrementalOverwrite: Property = + objectFactory.property(Boolean::class.java).convention(false) + val disableDependencyInjection: Property = + objectFactory.property(Boolean::class.java).convention(false) + val disableComposeDependencyInjection: Property = + objectFactory.property(Boolean::class.java).convention(true) + val disableRNBundleRetriever: Property = + objectFactory.property(Boolean::class.java).convention(false) + val instrumentOkHttp: Property = + objectFactory.property(Boolean::class.java).convention(DEFAULT_INSTRUMENT_OKHTTP) + val instrumentOnClick: Property = + objectFactory.property(Boolean::class.java).convention(DEFAULT_INSTRUMENT_ON_CLICK) + val instrumentOnLongClick: Property = + objectFactory.property(Boolean::class.java).convention(DEFAULT_INSTRUMENT_ON_LONG_CLICK) + val instrumentWebview: Property = + objectFactory.property(Boolean::class.java).convention(DEFAULT_INSTRUMENT_WEBVIEW) + val instrumentFirebaseMessaging: Property = + objectFactory.property(Boolean::class.java) + .convention(DEFAULT_INSTRUMENT_FIREBASE_MESSAGING) + + // It is now ignored because we're automatically detecting all native symbols. This can be removed. + @get:Deprecated("") + val customSymbolsDirectory: Property = + objectFactory.property(String::class.java).convention(DEFAULT_CUSTOM_SYMBOLS_DIRECTORY) + val classSkipList: ListProperty = + objectFactory.listProperty(String::class.java).convention(emptyList()) + + var variantFilter: Action? = null + + fun SwazzlerExtension.variantFilter(variantFilter: Action?) { + this.variantFilter = variantFilter + } + + /** + * @return true if the variant is disabled. + */ + fun isSwazzlingDisabled(variantName: String): Boolean { + val variant = Variant(variantName) + variantFilter?.execute(variant) + return !variant.enabled + } + + /** + * It determines if the plugin should be off for given variant. + * + * @return true if the plugin should be turned off for given variant. + */ + fun isPluginDisabledForVariant(variantName: String): Boolean { + val variant = Variant(variantName) + variantFilter?.execute(variant) + return variant.swazzlerOff + } + + /** + * Represents a build variant. + */ + class Variant internal constructor(val name: String) { + var enabled: Boolean = true + var swazzlerOff: Boolean = false + + fun setSwazzlingEnabled(enabled: Boolean) { + this.enabled = enabled + } + + fun disablePluginForVariant() { + this.swazzlerOff = true + } + } + + companion object { + const val DEFAULT_INSTRUMENT_OKHTTP: Boolean = true + const val DEFAULT_INSTRUMENT_ON_CLICK: Boolean = true + const val DEFAULT_INSTRUMENT_ON_LONG_CLICK: Boolean = true + const val DEFAULT_INSTRUMENT_WEBVIEW: Boolean = true + const val DEFAULT_INSTRUMENT_FIREBASE_MESSAGING: Boolean = false + private const val DEFAULT_CUSTOM_SYMBOLS_DIRECTORY = "" + } +} diff --git a/embrace-gradle-plugin/src/main/resources/META-INF/gradle-plugins/embrace-swazzler.properties b/embrace-gradle-plugin/src/main/resources/META-INF/gradle-plugins/embrace-swazzler.properties new file mode 100644 index 0000000000..0cd0cd6c86 --- /dev/null +++ b/embrace-gradle-plugin/src/main/resources/META-INF/gradle-plugins/embrace-swazzler.properties @@ -0,0 +1 @@ +implementation-class=io.embrace.android.gradle.plugin.EmbraceGradlePlugin \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/ResourceReader.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/ResourceReader.kt new file mode 100644 index 0000000000..651eedcd04 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/ResourceReader.kt @@ -0,0 +1,15 @@ +package io.embrace.android.gradle + +import java.io.InputStream + +object ResourceReader { + fun readResource(name: String): InputStream { + val classLoader = checkNotNull(javaClass.classLoader) + return classLoader.getResourceAsStream(name) + ?: error("Could not find resource '$name'") + } + + fun readResourceAsText(name: String): String { + return readResource(name).bufferedReader().readText() + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeConfigFileDirectory.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeConfigFileDirectory.kt new file mode 100644 index 0000000000..a4717c6500 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeConfigFileDirectory.kt @@ -0,0 +1,40 @@ +package io.embrace.android.gradle.fakes + +import org.gradle.api.file.Directory +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileTree +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import java.io.File + +class FakeConfigFileDirectory( + private val dirPath: String = "root", + private val hasConfigFile: Boolean = false +) : Directory { + var subDirectoriesWithConfigFiles: MutableSet = mutableSetOf() + + override fun getAsFile(): File = File(dirPath) + + override fun getAsFileTree(): FileTree { + TODO("Not yet implemented") + } + + override fun dir(path: String): Directory = FakeConfigFileDirectory( + "$dirPath/$path", + subDirectoriesWithConfigFiles.contains(path) + ) + + override fun dir(path: Provider): Provider { + TODO("Not yet implemented") + } + + override fun file(path: String): RegularFile = FakeRegularFile("$dirPath/$path", hasConfigFile) + + override fun file(path: Provider): Provider { + TODO("Not yet implemented") + } + + override fun files(vararg paths: Any?): FileCollection { + TODO("Not yet implemented") + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeEnvironment.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeEnvironment.kt new file mode 100644 index 0000000000..6cde9049d9 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeEnvironment.kt @@ -0,0 +1,13 @@ +package io.embrace.android.gradle.fakes + +import io.embrace.android.gradle.plugin.system.Environment +import java.util.concurrent.ConcurrentHashMap + +class FakeEnvironment(initialVariables: Map = emptyMap()) : Environment { + + val envVariables = ConcurrentHashMap().apply { + putAll(initialVariables) + } + + override fun getVariable(name: String): String? = envVariables[name] +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeFile.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeFile.kt new file mode 100644 index 0000000000..22a27a4590 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeFile.kt @@ -0,0 +1,7 @@ +package io.embrace.android.gradle.fakes + +import java.io.File + +class FakeFile(path: String, var exists: Boolean = true) : File(path) { + override fun exists(): Boolean = exists +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeRegularFile.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeRegularFile.kt new file mode 100644 index 0000000000..e98f0569bd --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/fakes/FakeRegularFile.kt @@ -0,0 +1,8 @@ +package io.embrace.android.gradle.fakes + +import org.gradle.api.file.RegularFile +import java.io.File + +class FakeRegularFile(private val path: String, private val exists: Boolean) : RegularFile { + override fun getAsFile(): File = FakeFile(path, exists) +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/TaskRegistrationUtilsTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/TaskRegistrationUtilsTest.kt new file mode 100644 index 0000000000..c3bf465ad2 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/TaskRegistrationUtilsTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.gradle.plugin + +import io.embrace.android.gradle.plugin.gradle.isTaskRegistered +import io.embrace.android.gradle.plugin.gradle.tryGetTaskProvider +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.util.capitalizedString +import io.mockk.every +import io.mockk.mockk +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class TaskRegistrationUtilsTest { + + private val project = ProjectBuilder.builder().build() + + @Test + fun `verify task is registered`() { + val variantName = "the-variant" + val taskName = "the-task" + project.tasks.register("$taskName${variantName.capitalizedString()}") + + assertTrue(project.isTaskRegistered(taskName, variantName)) + } + + @Test + fun `verify task is not registered`() { + val variantName = "the-variant" + val taskName = "the-task" + + assertFalse(project.isTaskRegistered(taskName, variantName)) + } + + @Test + fun `verify task provider is registered`() { + assertTrue(isTaskRegistered(mockk())) + } + + @Test + fun `verify task provider is not registered`() { + assertFalse(isTaskRegistered(null)) + } + + @Test + fun `get task provider successfully`() { + val variantName = "the-variant" + val variantData = mockk { + every { name } returns variantName + } + val taskName = "the-task" + project.tasks.register("$taskName${variantData.name.capitalizedString()}") + + assertNotNull(project.tryGetTaskProvider("$taskName${variantData.name.capitalizedString()}")) + } + + @Test + fun `for unknown task it should return null task provider`() { + assertNull(project.tryGetTaskProvider("unknown-task")) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImplTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImplTest.kt new file mode 100644 index 0000000000..19f8d8fe07 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/InstrumentationBehaviorImplTest.kt @@ -0,0 +1,124 @@ +package io.embrace.android.gradle.plugin.config + +import com.android.build.api.instrumentation.InstrumentationScope +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class InstrumentationBehaviorImplTest { + + private lateinit var project: Project + private lateinit var extension: SwazzlerExtension + private lateinit var behavior: InstrumentationBehavior + + @Before + fun setUp() { + project = ProjectBuilder.builder().build() + extension = project.extensions.create("swazzler", SwazzlerExtension::class.java) + behavior = InstrumentationBehaviorImpl(project, extension) + } + + @Test + fun `instrumentation scope default`() { + assertEquals(InstrumentationScope.ALL, behavior.scope) + } + + @Test + fun `instrumentation scope valid`() { + addGradleProperty(EMBRACE_INSTRUMENTATION_SCOPE, "project") + assertEquals(InstrumentationScope.PROJECT, behavior.scope) + } + + @Test + fun `instrumentation scope invalid`() { + addGradleProperty(EMBRACE_INSTRUMENTATION_SCOPE, "foo") + assertEquals(InstrumentationScope.ALL, behavior.scope) + } + + @Test + fun `invalidateBytecode default`() { + assertFalse(behavior.invalidateBytecode) + } + + @Test + fun `invalidateBytecode true`() { + extension.forceIncrementalOverwrite.set(true) + assertTrue(behavior.invalidateBytecode) + } + + @Test + fun `okHttpEnabled default`() { + assertTrue(behavior.okHttpEnabled) + } + + @Test + fun `okHttpEnabled false`() { + extension.instrumentOkHttp.set(false) + assertFalse(behavior.okHttpEnabled) + } + + @Test + fun `onClickEnabled default`() { + assertTrue(behavior.onClickEnabled) + } + + @Test + fun `onClickEnabled false`() { + extension.instrumentOnClick.set(false) + assertFalse(behavior.onClickEnabled) + } + + @Test + fun `onLongClickEnabled default`() { + assertTrue(behavior.onLongClickEnabled) + } + + @Test + fun `onLongClickEnabled false`() { + extension.instrumentOnLongClick.set(false) + assertFalse(behavior.onLongClickEnabled) + } + + @Test + fun `webviewEnabled default`() { + assertTrue(behavior.webviewEnabled) + } + + @Test + fun `webviewEnabled false`() { + extension.instrumentWebview.set(false) + assertFalse(behavior.webviewEnabled) + } + + @Test + fun `fcmPushNotificationsEnabled default`() { + assertFalse(behavior.fcmPushNotificationsEnabled) + } + + @Test + fun `fcmPushNotificationsEnabled false`() { + extension.instrumentFirebaseMessaging.set(true) + assertTrue(behavior.fcmPushNotificationsEnabled) + } + + @Test + fun `ignoredClasses default`() { + assertTrue(behavior.ignoredClasses.isEmpty()) + } + + @Test + fun `ignoredClasses override`() { + val values = listOf("foo", "bar") + extension.classSkipList.set(values) + assertEquals(values, behavior.ignoredClasses) + } + + private fun addGradleProperty(key: String, value: String) { + project.extensions.extraProperties[key] = value + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImplTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImplTest.kt new file mode 100644 index 0000000000..28b5ae8205 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/PluginBehaviorImplTest.kt @@ -0,0 +1,260 @@ +package io.embrace.android.gradle.plugin.config + +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.logging.LogLevel +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.File + +class PluginBehaviorImplTest { + + private lateinit var project: Project + private lateinit var extension: SwazzlerExtension + private lateinit var behavior: PluginBehavior + + @Before + fun setUp() { + project = ProjectBuilder.builder().build() + extension = project.extensions.create("swazzler", SwazzlerExtension::class.java) + behavior = PluginBehaviorImpl(project, extension) + } + + @Test + fun `log level default`() { + assertNull(behavior.logLevel) + } + + @Test + fun `log level INFO`() { + addGradleProperty(EMBRACE_LOG_LEVEL, "info") + assertEquals(LogLevel.INFO, behavior.logLevel) + } + + @Test + fun `log level WARN`() { + addGradleProperty(EMBRACE_LOG_LEVEL, "WARN") + assertEquals(LogLevel.WARN, behavior.logLevel) + } + + @Test + fun `log level ERROR`() { + addGradleProperty(EMBRACE_LOG_LEVEL, "ErRoR") + assertEquals(LogLevel.ERROR, behavior.logLevel) + } + + @Test + fun `log level invalid`() { + addGradleProperty(EMBRACE_LOG_LEVEL, "foo") + assertNull(behavior.logLevel) + } + + @Test + fun `telemetry disabled default`() { + assertFalse(behavior.isTelemetryDisabled) + } + + @Test + fun `telemetry disabled valid`() { + addGradleProperty(EMBRACE_DISABLE_COLLECT_BUILD_DATA, "true") + assertTrue(behavior.isTelemetryDisabled) + } + + @Test + fun `telemetry disabled invalid`() { + addGradleProperty(EMBRACE_DISABLE_COLLECT_BUILD_DATA, "foo") + assertFalse(behavior.isTelemetryDisabled) + } + + @Test + fun `unity edm enabled default`() { + assertFalse(behavior.isUnityEdmEnabled) + } + + @Test + fun `unity edm enabled valid`() { + addGradleProperty(EMBRACE_UNITY_EXTERNAL_DEPENDENCY_MANAGER, "true") + assertTrue(behavior.isUnityEdmEnabled) + } + + @Test + fun `unity edm enabled invalid`() { + addGradleProperty(EMBRACE_UNITY_EXTERNAL_DEPENDENCY_MANAGER, "foo") + assertFalse(behavior.isUnityEdmEnabled) + } + + @Test + fun `il2cpp upload enabled default`() { + assertFalse(behavior.isIl2CppMappingFilesUploadEnabled) + } + + @Test + fun `il2cpp upload enabled valid`() { + addGradleProperty(EMBRACE_UPLOAD_IL2CPP_MAPPING_FILES, "true") + assertTrue(behavior.isIl2CppMappingFilesUploadEnabled) + } + + @Test + fun `il2cpp upload enabled invalid`() { + addGradleProperty(EMBRACE_UPLOAD_IL2CPP_MAPPING_FILES, "foo") + assertFalse(behavior.isIl2CppMappingFilesUploadEnabled) + } + + @Test + fun `mapping file upload disabled default`() { + assertFalse(behavior.isUploadMappingFilesDisabled) + } + + @Test + fun `mapping file upload disabled valid`() { + addGradleProperty(EMBRACE_DISABLE_MAPPING_FILE_UPLOAD, "true") + assertTrue(behavior.isUploadMappingFilesDisabled) + } + + @Test + fun `mapping file upload disabled invalid`() { + addGradleProperty(EMBRACE_DISABLE_MAPPING_FILE_UPLOAD, "foo") + assertFalse(behavior.isUploadMappingFilesDisabled) + } + + @Test + fun `base url default`() { + assertEquals(DEFAULT_SYMBOL_STORE_HOST_URL, behavior.baseUrl) + } + + @Test + fun `base url http`() { + val httpExample = "http://example.com" + addGradleProperty(EMBRACE_BASE_URL, httpExample) + assertEquals(httpExample, behavior.baseUrl) + } + + @Test + fun `base url https`() { + val httpsExample = "https://example.com" + addGradleProperty(EMBRACE_BASE_URL, httpsExample) + assertEquals(httpsExample, behavior.baseUrl) + } + + @Test + fun `base url without protocol`() { + val domainExample = "example.com" + addGradleProperty(EMBRACE_BASE_URL, domainExample) + assertEquals("https://$domainExample", behavior.baseUrl) + } + + @Test + fun `react native project false`() { + val rnDir = findRnNodeModulesDir() + rnDir.deleteRecursively() + assertFalse(behavior.isReactNativeProject) + } + + @Test + fun `react native project true`() { + val rnDir = findRnNodeModulesDir() + rnDir.mkdirs() + assertTrue(behavior.isReactNativeProject) + } + + @Test + fun `autoAddEmbraceDependencies default`() { + assertTrue(behavior.autoAddEmbraceDependencies) + } + + @Test + fun `autoAddEmbraceDependencies disabled`() { + extension.disableDependencyInjection.set(true) + assertFalse(behavior.autoAddEmbraceDependencies) + } + + @Test + fun `autoAddEmbraceDependency disabled`() { + extension.disableDependencyInjection.set(true) + assertFalse(behavior.autoAddEmbraceDependencies) + } + + /** + * If Unity EDM is enabled, Embrace dependency should not be auto-added. + */ + @Test + fun `autoAddEmbraceComposeDependency unity edm override`() { + addGradleProperty(EMBRACE_UNITY_EXTERNAL_DEPENDENCY_MANAGER, "true") + assertFalse(behavior.autoAddEmbraceDependencies) + } + + @Test + fun `autoAddEmbraceComposeDependency default`() { + assertFalse(behavior.autoAddEmbraceComposeDependency) + } + + @Test + fun `autoAddEmbraceComposeDependency enabled`() { + extension.disableComposeDependencyInjection.set(false) + assertTrue(behavior.autoAddEmbraceComposeDependency) + } + + @Test + fun `customSymbolDirectory default`() { + assertEquals("", behavior.customSymbolsDirectory) + } + + @Suppress("DEPRECATION") + @Test + fun `customSymbolDirectory override`() { + val dir = "/foo" + extension.customSymbolsDirectory.set(dir) + assertEquals(dir, behavior.customSymbolsDirectory) + } + + @Test + fun `instrumentation enabled for variant`() { + assertFalse(behavior.isInstrumentationDisabledForVariant("foo")) + } + + @Test + fun `instrumentation disabled for variant`() { + val disabledVariant = "foo" + extension.variantFilter = Action { + if (it.name == disabledVariant) { + it.enabled = false + } + } + assertTrue(behavior.isInstrumentationDisabledForVariant(disabledVariant)) + assertFalse(behavior.isInstrumentationDisabledForVariant("bar")) + } + + @Test + fun `plugin enabled for variant`() { + assertFalse(behavior.isPluginDisabledForVariant("foo")) + } + + @Test + fun `plugin disabled for variant`() { + val disabledVariant = "foo" + extension.variantFilter = Action { + if (it.name == disabledVariant) { + it.swazzlerOff = true + } + } + assertTrue(behavior.isPluginDisabledForVariant(disabledVariant)) + assertFalse(behavior.isPluginDisabledForVariant("bar")) + } + + private fun addGradleProperty(key: String, value: String) { + project.extensions.extraProperties[key] = value + } + + private fun findRnNodeModulesDir(): File { + val rootFile = project.layout.projectDirectory.asFile.parentFile?.parentFile + ?: error("Parent directory of project root is null") + + return File("${File("${rootFile.path}/node_modules").path}/react-native") + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/VariantConfigDeserializerTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/VariantConfigDeserializerTest.kt new file mode 100644 index 0000000000..2ce0410ac0 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/VariantConfigDeserializerTest.kt @@ -0,0 +1,17 @@ +package io.embrace.android.gradle.plugin.config + +import io.embrace.android.gradle.ResourceReader +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import org.junit.Assert.assertNotNull +import org.junit.Test + +class VariantConfigDeserializerTest { + + @Test + fun testSDKConfiguration() { + val configFile = ResourceReader.readResourceAsText("config_file_expected.json") + val obj = MoshiSerializer().fromJson(configFile, VariantConfig::class.java) + assertNotNull(obj) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderForValueSourceTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderForValueSourceTest.kt new file mode 100644 index 0000000000..31cf0adcee --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderForValueSourceTest.kt @@ -0,0 +1,63 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import org.gradle.testfixtures.ProjectBuilder +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.BeforeClass +import org.junit.Test + +class VariantConfigBuilderForValueSourceTest { + + companion object { + private val project = ProjectBuilder.builder().build() + private val variant = mockk(relaxed = true) { + every { productFlavors } returns emptyList() + } + private val projectDirectory = project.layout.projectDirectory + private val variantConfigurationBuilder = EmbraceVariantConfigurationBuilderForValueSource( + projectDirectory, + project.providers + ) + private val expectedVariantConfig = mockk(relaxed = true) + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkObject(EmbraceVariantConfigurationFileBuildStrategy) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkObject(EmbraceVariantConfigurationFileBuildStrategy) + } + } + + @Test + fun `variant configuration could not be built, it should not throw exception because it is optional`() { + every { EmbraceVariantConfigurationFileBuildStrategy.build(any(), projectDirectory, any()) } returns null + + assertNotNull(variantConfigurationBuilder.buildVariantConfiguration(variant)) + } + + @Test + fun `variant configuration built from file`() { + every { EmbraceVariantConfigurationFileBuildStrategy.build(any(), projectDirectory, any()) } returns + expectedVariantConfig + + val variantConfigurationProvider = variantConfigurationBuilder.buildVariantConfiguration( + variant + ) + + verify { EmbraceVariantConfigurationFileBuildStrategy.build(any(), projectDirectory, any()) } + assertEquals(expectedVariantConfig, variantConfigurationProvider.get()) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderTest.kt new file mode 100644 index 0000000000..c5e302ede9 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigBuilderTest.kt @@ -0,0 +1,68 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.gradle.GradleCompatibilityHelper +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import org.gradle.api.provider.Provider +import org.gradle.testfixtures.ProjectBuilder +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.BeforeClass +import org.junit.Test + +class VariantConfigBuilderTest { + + companion object { + + private val project = ProjectBuilder.builder().build() + private val mockVariant: AndroidCompactedVariantData = mockk { + every { name } returns "variantName" + } + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkObject(GradleCompatibilityHelper) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkObject(GradleCompatibilityHelper) + } + } + + @Test + fun `if variant configuration is not present it should not throw exception because it is optional`() { + val builder = object : EmbraceVariantConfigurationBuilder() { + override fun buildProvider(variant: AndroidCompactedVariantData): Provider { + return project.provider { + null + } + } + } + + assertNotNull(builder.buildVariantConfiguration(mockVariant)) + } + + @Test + fun `build variant configuration successfully`() { + val variantConfig = mockk(relaxed = true) + + val builder = object : EmbraceVariantConfigurationBuilder() { + override fun buildProvider(variant: AndroidCompactedVariantData): Provider { + return project.provider { + variantConfig + } + } + } + + val result = builder.buildVariantConfiguration(mockVariant) + assertEquals(result.get(), variantConfig) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileBuildStrategyTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileBuildStrategyTest.kt new file mode 100644 index 0000000000..ab0c660096 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileBuildStrategyTest.kt @@ -0,0 +1,106 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import org.gradle.testfixtures.ProjectBuilder +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.BeforeClass +import org.junit.Test +import java.io.File + +class VariantConfigFileBuildStrategyTest { + + companion object { + private val project = ProjectBuilder.builder().build() + private val variantInfo = mockk(relaxed = true) + private val projectDirectory = project.layout.projectDirectory + private val configFileFinder = mockk() + + @BeforeClass + @JvmStatic + fun beforeClass() { + mockkObject(VariantConfigurationValidator) + } + + @AfterClass + @JvmStatic + fun afterClass() { + unmockkObject(VariantConfigurationValidator) + } + } + + @Test(expected = RuntimeException::class) + fun `build with no config file finders should throw exception`() { + EmbraceVariantConfigurationFileBuildStrategy.build( + variantInfo, + projectDirectory, + emptyList() + ) + } + + @Test + fun `build with config file finders, but file was not found, it should not throw exception because file is optional`() { + every { configFileFinder.fetchFile() } returns null + + val variantConfiguration = EmbraceVariantConfigurationFileBuildStrategy.build( + variantInfo, + projectDirectory, + listOf(configFileFinder) + ) + + assertNull(variantConfiguration) + } + + @Test + fun `build VariantConfiguration successfully`() { + val expectedVariantConfig = mockk() + every { configFileFinder.fetchFile() } returns + File(javaClass.classLoader.getResource("config_file_expected.json").file) + every { + VariantConfigurationValidator.validate( + any(), + VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + any() + ) + } returns expectedVariantConfig + + val variantConfiguration = EmbraceVariantConfigurationFileBuildStrategy.build( + variantInfo, + projectDirectory, + listOf(configFileFinder) + ) + + assertEquals(expectedVariantConfig, variantConfiguration) + verify { configFileFinder.fetchFile() } + verify { + VariantConfigurationValidator.validate( + any(), + VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + any() + ) + } + } + + @Test + fun `for empty embrace-config json file it should not throw exception because all values are now optional`() { + every { configFileFinder.fetchFile() } returns + File(javaClass.classLoader.getResource("config_file_empty.json").file) + + val variantConfiguration = EmbraceVariantConfigurationFileBuildStrategy.build( + variantInfo, + projectDirectory, + listOf(configFileFinder) + ) + + verify { configFileFinder.fetchFile() } + assertNotNull(variantConfiguration) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileFinderTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileFinderTest.kt new file mode 100644 index 0000000000..6ef6231bbc --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigFileFinderTest.kt @@ -0,0 +1,109 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.fakes.FakeConfigFileDirectory +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class VariantConfigFileFinderTest { + private lateinit var projectDir: FakeConfigFileDirectory + private lateinit var fileFinder: VariantConfigurationFileFinder + + @Before + fun before() { + projectDir = FakeConfigFileDirectory() + projectDir.subDirectoriesWithConfigFiles.add("src") + fileFinder = VariantConfigurationFileFinder(projectDir, listOf("src")) + } + + @Test + fun `return correct file if config file exists`() { + assertNotNull(fileFinder.fetchFile()) + } + + @Test + fun `if no file locations then return null`() { + fileFinder = VariantConfigurationFileFinder(projectDir, emptyList()) + assertNull(fileFinder.fetchFile()) + } + + @Test + fun `if location exist but file does not exist then return null`() { + projectDir.subDirectoriesWithConfigFiles.clear() + assertNull(fileFinder.fetchFile()) + } + + @Test + fun `find config file in default directory`() { + val path = "src/main" + projectDir.subDirectoriesWithConfigFiles.add(path) + validateConfigFile(defaultConfigurationFileFinder(projectDir), path) + } + + @Test + fun `find config file in build type variant directory`() { + val path = "src/${fakeVariantInfo.buildTypeName}" + projectDir.subDirectoriesWithConfigFiles.add(path) + validateConfigFile(variantBuildTypeConfigurationFileFinder(fakeVariantInfo, projectDir), path) + } + + @Test + fun `find config file in flavor variant directory`() { + val path = "src/${fakeVariantInfo.flavorName}" + projectDir.subDirectoriesWithConfigFiles.add(path) + validateConfigFile(variantFlavorConfigurationFileFinder(fakeVariantInfo, projectDir), path) + } + + @Test + fun `find config file in name variant directory`() { + val path = "src/${fakeVariantInfo.name}" + projectDir.subDirectoriesWithConfigFiles.add(path) + validateConfigFile(variantNameConfigurationFileFinder(fakeVariantInfo, projectDir), path) + } + + @Test + fun `config file not found when using product flavor if there are no product flavors`() { + val variantInfo = fakeVariantInfo.copy(productFlavors = emptyList()) + val fileFinder = variantProductFlavorsConfigurationFileFinder(variantInfo, projectDir) + assertNull(fileFinder.fetchFile()) + } + + @Test + fun `config file in first product flavor location used if multiple are defined`() { + val path = "src/${fakeVariantInfo.productFlavors.first()}" + fakeVariantInfo.productFlavors.map { "src/$it" }.forEach { + projectDir.subDirectoriesWithConfigFiles.add(it) + } + + validateConfigFile(variantProductFlavorsConfigurationFileFinder(fakeVariantInfo, projectDir), path) + } + + @Test + fun `test directory for product flavor if first one doesn't exist`() { + val path = "src/${fakeVariantInfo.productFlavors.last()}" + projectDir.subDirectoriesWithConfigFiles.add(path) + validateConfigFile(variantProductFlavorsConfigurationFileFinder(fakeVariantInfo, projectDir), path) + } + + private fun validateConfigFile(fileFinder: VariantConfigurationFileFinder, path: String) { + assertEquals( + "${projectDir.asFile.path}/$path/$VARIANT_CONFIG_FILE_NAME", + checkNotNull(fileFinder.fetchFile()).path + ) + } + + private companion object { + val fakeVariantInfo = AndroidCompactedVariantData( + name = "variant-name", + flavorName = "flavor-name", + buildTypeName = "buildType-name", + isBuildTypeDebuggable = false, + versionName = "1.0", + productFlavors = listOf("product-flavor", "2nd-product-flavor"), + sourceMapPath = "source" + ) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigValidatorTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigValidatorTest.kt new file mode 100644 index 0000000000..cc86e25339 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/config/variant/VariantConfigValidatorTest.kt @@ -0,0 +1,113 @@ +package io.embrace.android.gradle.plugin.config.variant + +import io.embrace.android.gradle.fakes.FakeEnvironment +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.system.Environment +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class VariantConfigValidatorTest { + + @Test + fun `do not fail if appId and apiToken are null because they are optional`() { + val configuration = getVariantConfiguration() + + VariantConfigurationValidator.validate( + configuration, + VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + FakeEnvironment() + ) + } + + @Test + fun `fail if appId length is not valid`() { + val configuration = getVariantConfiguration( + appId = "appIdVeryLong" + ) + + assertValidationFailure( + configuration, + VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + "app_id must contain exactly" + ) + + assertValidationFailure( + configuration, + VariantConfigurationValidator.VariantConfigurationSourceType.EXTENSION, + "appId must contain exactly" + ) + } + + @Test + fun `fail if apiToken length is not valid`() { + val configuration = getVariantConfiguration( + appId = "abcde", + apiToken = "asdf", + ) + + assertValidationFailure( + configuration, + VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + "api_token must contain exactly", + FakeEnvironment() + ) + + assertValidationFailure( + configuration, + VariantConfigurationValidator.VariantConfigurationSourceType.EXTENSION, + expectedMessageInException = "apiToken must contain exactly", + FakeEnvironment() + ) + } + + @Test + fun `fail if apiToken is not valid and env api token length is not valid`() { + val configuration = getVariantConfiguration( + appId = "abcde" + ) + + assertValidationFailure( + configuration = configuration, + sourceType = VariantConfigurationValidator.VariantConfigurationSourceType.CONFIG_FILE, + expectedMessageInException = "api_token must contain exactly", + environment = FakeEnvironment(mapOf(EMBRACE_API_TOKEN_ENV_KEY to "asdf")) + ) + + assertValidationFailure( + configuration = configuration, + sourceType = VariantConfigurationValidator.VariantConfigurationSourceType.EXTENSION, + expectedMessageInException = "apiToken must contain exactly", + environment = FakeEnvironment(mapOf(EMBRACE_API_TOKEN_ENV_KEY to "asdf")) + ) + } + + private fun getVariantConfiguration( + appId: String? = null, + apiToken: String? = null, + ndkEnabled: Boolean? = null, + ) = EmbraceVariantConfig( + appId, + apiToken, + ndkEnabled, + null, + null + ) + + private fun assertValidationFailure( + configuration: EmbraceVariantConfig, + sourceType: VariantConfigurationValidator.VariantConfigurationSourceType, + expectedMessageInException: String, + environment: Environment = FakeEnvironment() + ) { + val exception = assertThrows(IllegalArgumentException::class.java) { + VariantConfigurationValidator.validate( + configuration = configuration, + sourceType = sourceType, + environment = environment + ) + } + + assertTrue(exception.message?.contains(expectedMessageInException) ?: false) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadataTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadataTest.kt new file mode 100644 index 0000000000..05c3db5fc6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/dependency/EmbraceDependencyMetadataTest.kt @@ -0,0 +1,23 @@ +package io.embrace.android.gradle.plugin.dependency + +import io.embrace.embrace_gradle_plugin.BuildConfig +import org.junit.Assert.assertEquals +import org.junit.Test + +class EmbraceDependencyMetadataTest { + + @Test + fun `verify correct values`() { + val customVersion = "customVersion" + + val defaultCore = EmbraceDependencyMetadata.Core() + val customVersionCore = EmbraceDependencyMetadata.Core(customVersion) + + assertEquals("io.embrace", defaultCore.group) + assertEquals("io.embrace", customVersionCore.group) + assertEquals("embrace-android-sdk", defaultCore.artefact) + assertEquals("embrace-android-sdk", customVersionCore.artefact) + assertEquals(BuildConfig.VERSION, defaultCore.version) + assertEquals(customVersion, customVersionCore.version) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/AsmApiConstant.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/AsmApiConstant.kt new file mode 100644 index 0000000000..35e5253bde --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/AsmApiConstant.kt @@ -0,0 +1,6 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import org.objectweb.asm.Opcodes + +// checked AGP 7.4.2 in com.android.build.gradle.internal.instrumentation.InstrumentationUtils +internal const val ASM_API_VERSION = Opcodes.ASM9 diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryTest.kt new file mode 100644 index 0000000000..022a147dc0 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/EmbraceClassVisitorFactoryTest.kt @@ -0,0 +1,203 @@ +package io.embrace.android.gradle.plugin.instrumentation + +import com.android.build.api.instrumentation.ClassContext +import io.embrace.android.gradle.plugin.instrumentation.config.visitor.ConfigInstrumentationClassVisitor +import io.embrace.android.gradle.plugin.instrumentation.fakes.TestBytecodeInstrumentationParams +import io.embrace.android.gradle.plugin.instrumentation.fakes.TestClassContext +import io.embrace.android.gradle.plugin.instrumentation.fakes.TestClassData +import io.embrace.android.gradle.plugin.instrumentation.fakes.TestClassVisitor +import io.embrace.android.gradle.plugin.instrumentation.fakes.TestVisitorFactoryImpl +import io.embrace.android.gradle.plugin.instrumentation.visitor.FirebaseMessagingServiceClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OkHttpClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.OnLongClickClassAdapter +import io.embrace.android.gradle.plugin.instrumentation.visitor.WebViewClientClassAdapter +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertFalse +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test + +class EmbraceClassVisitorFactoryTest { + + private val clzDataString = TestClassData(checkNotNull(String::class.qualifiedName)) + private val clzDataBool = TestClassData(checkNotNull(Boolean::class.qualifiedName)) + + /** + * Verifies that the OnClick visitor should be returned if there is nothing to instrument, + * and that it should chain the original visitor. + */ + @Test + fun testDefaultClassVisitorReturned() { + val visitor = TestClassVisitor() + val ctx = TestClassContext(clzDataString) + val returningVisitor = TestVisitorFactoryImpl().createClassVisitor(ctx, visitor) + check(returningVisitor is OnClickClassAdapter) + check(returningVisitor.nextClassVisitor is OnLongClickClassAdapter) + assertSame(visitor, returningVisitor.nextClassVisitor.nextClassVisitor) + } + + @Test + fun testWebViewClassVisitorReturned() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("android.webkit.WebViewClient") + val returningVisitor = TestVisitorFactoryImpl().createClassVisitor(ctx, visitor) + check(returningVisitor is OnClickClassAdapter) + check(returningVisitor.nextClassVisitor is OnLongClickClassAdapter) + check(returningVisitor.nextClassVisitor.nextClassVisitor is WebViewClientClassAdapter) + assertSame(visitor, returningVisitor.nextClassVisitor.nextClassVisitor.nextClassVisitor) + } + + @Test + fun testConfigClassVisitorReturned() { + val visitor = TestClassVisitor() + val ctx = + createMockClassContext( + "io.embrace.android.embracesdk.internal.config.instrumented.EnabledFeatureConfigImpl" + ) + val returningVisitor = TestVisitorFactoryImpl().createClassVisitor(ctx, visitor) + check(returningVisitor is OnClickClassAdapter) + check(returningVisitor.nextClassVisitor is OnLongClickClassAdapter) + check(returningVisitor.nextClassVisitor.nextClassVisitor is ConfigInstrumentationClassVisitor) + } + + @Test + fun testOkHttpClassVisitorReturned() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("okhttp3.OkHttpClient\$Builder") + val returningVisitor = TestVisitorFactoryImpl().createClassVisitor(ctx, visitor) + check(returningVisitor is OnClickClassAdapter) + check(returningVisitor.nextClassVisitor is OnLongClickClassAdapter) + check(returningVisitor.nextClassVisitor.nextClassVisitor is OkHttpClassAdapter) + assertSame(visitor, returningVisitor.nextClassVisitor.nextClassVisitor.nextClassVisitor) + } + + @Test + fun testFCMClassVisitorReturned() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("com.google.firebase.messaging.FirebaseMessagingService") + val params = TestBytecodeInstrumentationParams(instrumentFirebaseMessaging = true) + val returningVisitor = TestVisitorFactoryImpl(params = params).createClassVisitor(ctx, visitor) + check(returningVisitor is OnClickClassAdapter) + check(returningVisitor.nextClassVisitor is OnLongClickClassAdapter) + check(returningVisitor.nextClassVisitor.nextClassVisitor is FirebaseMessagingServiceClassAdapter) + assertSame(visitor, returningVisitor.nextClassVisitor.nextClassVisitor.nextClassVisitor) + } + + @Test + fun testAsmApiEnabled() { + val factory = TestVisitorFactoryImpl(params = TestBytecodeInstrumentationParams(disabled = false)) + assertTrue(factory.isInstrumentable(clzDataString)) + assertTrue(factory.isInstrumentable(clzDataBool)) + } + + @Test + fun testAsmApiDisabled() { + val factory = TestVisitorFactoryImpl(params = TestBytecodeInstrumentationParams(disabled = true)) + assertFalse(factory.isInstrumentable(clzDataString)) + assertFalse(factory.isInstrumentable(clzDataBool)) + } + + @Test + fun testClassFiltering() { + val filter = ClassInstrumentationFilter(mutableListOf("kotlin.Boolean")) + val params = TestBytecodeInstrumentationParams(disabled = false, classInstrumentationFilter = filter) + val factory = TestVisitorFactoryImpl(params = params) + assertTrue(factory.isInstrumentable(clzDataString)) + assertFalse(factory.isInstrumentable(clzDataBool)) + } + + @Test + fun testOnClickVisitorDisabled() { + val visitor = TestClassVisitor() + val ctx = TestClassContext(clzDataString) + val config = createInstrumentationConfig(instrumentOnClick = false) + val observed = fetchClassVisitor(config, ctx, visitor) + assertTrue(observed is OnLongClickClassAdapter) + } + + @Test + fun testOnLongClickVisitorDisabled() { + val visitor = TestClassVisitor() + val ctx = TestClassContext(clzDataString) + val config = createInstrumentationConfig(instrumentOnLongClick = false) + val observed = fetchClassVisitor(config, ctx, visitor) + check(observed is OnClickClassAdapter) + check(observed.nextClassVisitor is TestClassVisitor) + } + + @Test + fun testWebViewClassVisitorDisabled() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("android.webkit.WebViewClient") + val config = createInstrumentationConfig( + instrumentOnClick = false, + instrumentOnLongClick = false, + instrumentWebview = false + ) + val observed = fetchClassVisitor(config, ctx, visitor) + assertSame(visitor, observed) + } + + @Test + fun testOkHttpClassVisitorDisabled() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("okhttp3.OkHttpClient\$Builder") + val config = createInstrumentationConfig( + instrumentOnClick = false, + instrumentOnLongClick = false, + instrumentOkHttp = false + ) + val observed = fetchClassVisitor(config, ctx, visitor) + assertSame(visitor, observed) + } + + @Test + fun testFcmClassVisitorDisabled() { + val visitor = TestClassVisitor() + val ctx = createMockClassContext("com.google.firebase.messaging.FirebaseMessagingService") + val config = createInstrumentationConfig( + instrumentOnClick = false, + instrumentOnLongClick = false, + instrumentFirebaseMessaging = false, + ) + val observed = fetchClassVisitor(config, ctx, visitor) + assertSame(visitor, observed) + } + + private fun fetchClassVisitor( + testParams: TestBytecodeInstrumentationParams, + ctx: ClassContext, + visitor: TestClassVisitor + ) = TestVisitorFactoryImpl(params = testParams).createClassVisitor( + ctx, + visitor + ) + + private fun createInstrumentationConfig( + instrumentOkHttp: Boolean = true, + instrumentOnClick: Boolean = true, + instrumentOnLongClick: Boolean = true, + instrumentWebview: Boolean = true, + instrumentFirebaseMessaging: Boolean = true + ): TestBytecodeInstrumentationParams { + return TestBytecodeInstrumentationParams( + instrumentOkHttp = instrumentOkHttp, + instrumentOnClick = instrumentOnClick, + instrumentOnLongClick = instrumentOnLongClick, + instrumentWebview = instrumentWebview, + instrumentFirebaseMessaging = instrumentFirebaseMessaging + ) + } + + private fun createMockClassContext(clzName: String): ClassContext { + val ctx = mockk { + every { currentClassData } returns mockk { + every { className } returns clzName + every { superClasses } returns listOf(clzName) + } + } + return ctx + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/InstrumentedConfigClassVisitorFactoryTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/InstrumentedConfigClassVisitorFactoryTest.kt new file mode 100644 index 0000000000..4206de717a --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/InstrumentedConfigClassVisitorFactoryTest.kt @@ -0,0 +1,50 @@ +package io.embrace.android.gradle.plugin.instrumentation.config + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.visitor.ConfigInstrumentationClassVisitor +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class InstrumentedConfigClassVisitorFactoryTest { + + @Test + fun createClassVisitor() { + val factory = ConfigClassVisitorFactory + val api = ASM_API_VERSION + val pkg = "io.embrace.android.embracesdk.internal.config.instrumented" + val cfg = VariantConfig("", null, null, null, null, null) + + assertNull(factory.createClassVisitor("", cfg, api, null)) + assertNull(factory.createClassVisitor("java.lang.Boolean", cfg, api, null)) + assertTrue( + factory.createClassVisitor("$pkg.BaseUrlConfigImpl", cfg, api, null) is ConfigInstrumentationClassVisitor + ) + assertTrue( + factory.createClassVisitor( + "$pkg.EnabledFeatureConfigImpl", + cfg, + api, + null + ) is ConfigInstrumentationClassVisitor + ) + assertTrue( + factory.createClassVisitor( + "$pkg.NetworkCaptureConfigImpl", + cfg, + api, + null + ) is ConfigInstrumentationClassVisitor + ) + assertTrue( + factory.createClassVisitor("$pkg.ProjectConfigImpl", cfg, api, null) is ConfigInstrumentationClassVisitor + ) + assertTrue( + factory.createClassVisitor("$pkg.RedactionConfigImpl", cfg, api, null) is ConfigInstrumentationClassVisitor + ) + assertTrue( + factory.createClassVisitor("$pkg.SessionConfigImpl", cfg, api, null) is ConfigInstrumentationClassVisitor + ) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClassTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClassTest.kt new file mode 100644 index 0000000000..ee782dc179 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/InstrumentedConfigClassTest.kt @@ -0,0 +1,128 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import io.embrace.android.gradle.plugin.instrumentation.config.BooleanReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.IntReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.LongReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.MapReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringReturnValueMethodVisitor +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test +import org.objectweb.asm.MethodVisitor + +class InstrumentedConfigClassTest { + + private val nextVisitor: MethodVisitor = mockk(relaxed = true) + private val api = ASM_API_VERSION + + private val cfg = modelSdkConfigClass { + boolMethod("getBool") { true } + boolMethod("getNullBool") { null } + intMethod("getInt") { 5 } + intMethod("getNullInt") { null } + longMethod("getLong") { 150L } + longMethod("getNullLong") { null } + stringMethod("getString") { "hello" } + stringMethod("getNullString") { null } + stringListMethod("getStringList") { listOf("hello") } + stringListMethod("getNullStringList") { null } + mapMethod("getMap") { mapOf("a" to "1") } + mapMethod("getNullMap") { null } + } + + @Test + fun `test bool`() { + val visitor = cfg.getMethodVisitor("getBool", "()Z", api, nextVisitor) + assertEquals(true, (visitor as BooleanReturnValueMethodVisitor).replacedValue) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()Z", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getBool", "()J", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullBool", "()Z", api, nextVisitor)) + } + + @Test + fun `test int`() { + val visitor = cfg.getMethodVisitor("getInt", "()I", api, nextVisitor) + assertEquals(5, (visitor as IntReturnValueMethodVisitor).replacedValue) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()I", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getInt", "()Z", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullInt", "()I", api, nextVisitor)) + } + + @Test + fun `test long`() { + val visitor = cfg.getMethodVisitor("getLong", "()J", api, nextVisitor) + assertEquals(150L, (visitor as LongReturnValueMethodVisitor).replacedValue) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()J", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getLong", "()Z", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullLong", "()J", api, nextVisitor)) + } + + @Test + fun `test string`() { + val visitor = cfg.getMethodVisitor("getString", "()Ljava/lang/String;", api, nextVisitor) + assertEquals("hello", (visitor as StringReturnValueMethodVisitor).replacedValue) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()Ljava/lang/String;", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getString", "()Z", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullString", "()Ljava/lang/String;", api, nextVisitor)) + } + + @Test + fun `test string list`() { + val visitor = cfg.getMethodVisitor("getStringList", "()Ljava/util/List;", api, nextVisitor) + assertEquals( + listOf("hello"), + (visitor as io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor).replacedValue + ) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()Ljava/util/List;", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getStringList", "()Z", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullStringList", "()Ljava/util/List;", api, nextVisitor)) + } + + @Test + fun `test map`() { + val visitor = cfg.getMethodVisitor("getMap", "()Ljava/util/Map;", api, nextVisitor) + assertEquals(mapOf("a" to "1"), (visitor as MapReturnValueMethodVisitor).replacedValue) + + // name doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("someFunction", "()Ljava/util/Map;", api, nextVisitor)) + + // descriptor doesn't match + assertSame(nextVisitor, cfg.getMethodVisitor("getMap", "()Z", api, nextVisitor)) + + // no config value supplied + assertSame(nextVisitor, cfg.getMethodVisitor("getNullMap", "()Ljava/util/Map;", api, nextVisitor)) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..b7bf9ce0a0 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/BaseUrlConfigInstrumentationKtTest.kt @@ -0,0 +1,55 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.BaseUrlLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.junit.Test + +class BaseUrlConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null + ) + + private val methods = listOf( + ConfigMethod("getConfig", "()Ljava/lang/String;", "config.example.com"), + ConfigMethod("getData", "()Ljava/lang/String;", "data.example.com") + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createBaseUrlConfigInstrumentation(cfg) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = createBaseUrlConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + null, + SdkLocalConfig( + baseUrls = BaseUrlLocalConfig( + config = "config.example.com", + data = "data.example.com" + ) + ), + null + ) + ) + ) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigInstrumentationAssertions.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigInstrumentationAssertions.kt new file mode 100644 index 0000000000..a45fe00414 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigInstrumentationAssertions.kt @@ -0,0 +1,40 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import io.embrace.android.gradle.plugin.instrumentation.config.BooleanReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.IntReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.LongReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.MapReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.StringReturnValueMethodVisitor +import io.embrace.android.gradle.plugin.instrumentation.config.arch.InstrumentedConfigClass +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.objectweb.asm.MethodVisitor + +private val nextVisitor: MethodVisitor = mockk(relaxed = true) + +fun verifyConfigMethodVisitor( + instrumentation: InstrumentedConfigClass, + method: ConfigMethod +) { + val visitor = instrumentation.getMethodVisitor( + method.name, + method.descriptor, + ASM_API_VERSION, + nextVisitor + ) + assertEquals( + "Expected ${method.name} to return ${method.result}", + method.result, + when (visitor) { + is BooleanReturnValueMethodVisitor -> visitor.replacedValue + is IntReturnValueMethodVisitor -> visitor.replacedValue + is LongReturnValueMethodVisitor -> visitor.replacedValue + is StringReturnValueMethodVisitor -> visitor.replacedValue + is io.embrace.android.gradle.plugin.instrumentation.config.StringListReturnValueMethodVisitor -> visitor.replacedValue + is MapReturnValueMethodVisitor -> visitor.replacedValue + else -> null + } + ) +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigMethod.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigMethod.kt new file mode 100644 index 0000000000..f9adc1b39f --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ConfigMethod.kt @@ -0,0 +1,7 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +data class ConfigMethod( + val name: String, + val descriptor: String, + val result: Any? = null +) diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..a42f3e7cf1 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/EnabledFeatureConfigInstrumentationKtTest.kt @@ -0,0 +1,227 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.AnrLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.AppExitInfoLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.AppLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.AutomaticDataCaptureLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.BackgroundActivityLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.ComposeLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.CrashHandlerLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.NetworkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.TapsLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.ViewLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.WebViewLocalConfig +import org.junit.Test + +class EnabledFeatureConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null + ) + + private val methods = listOf( + ConfigMethod("isUnityAnrCaptureEnabled", "()Z", true), + ConfigMethod("isActivityBreadcrumbCaptureEnabled", "()Z", true), + ConfigMethod("isComposeClickCaptureEnabled", "()Z", true), + ConfigMethod("isViewClickCoordinateCaptureEnabled", "()Z", true), + ConfigMethod("isMemoryWarningCaptureEnabled", "()Z", true), + ConfigMethod("isPowerSaveModeCaptureEnabled", "()Z", true), + ConfigMethod("isNetworkConnectivityCaptureEnabled", "()Z", true), + ConfigMethod("isAnrCaptureEnabled", "()Z", true), + ConfigMethod("isDiskUsageCaptureEnabled", "()Z", true), + ConfigMethod("isJvmCrashCaptureEnabled", "()Z", true), + ConfigMethod("isNativeCrashCaptureEnabled", "()Z", true), + ConfigMethod("isAeiCaptureEnabled", "()Z", true), + ConfigMethod("is3rdPartySigHandlerDetectionEnabled", "()Z", true), + ConfigMethod("isBackgroundActivityCaptureEnabled", "()Z", true), + ConfigMethod("isWebViewBreadcrumbCaptureEnabled", "()Z", true), + ConfigMethod("isWebViewBreadcrumbQueryParamCaptureEnabled", "()Z", true), + ConfigMethod("isFcmPiiDataCaptureEnabled", "()Z", true), + ConfigMethod("isRequestContentLengthCaptureEnabled", "()Z", true), + ConfigMethod("isHttpUrlConnectionCaptureEnabled", "()Z", true), + ConfigMethod("isNetworkSpanForwardingEnabled", "()Z", true), + ConfigMethod("isUiLoadTracingEnabled", "()Z", true), + ConfigMethod("isUiLoadTracingTraceAll", "()Z", true), + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createEnabledFeatureConfigInstrumentation(cfg) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = createEnabledFeatureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + true, + SdkLocalConfig( + anr = AnrLocalConfig( + captureUnityThread = true + ), + app = AppLocalConfig( + reportDiskUsage = true + ), + appExitInfoConfig = AppExitInfoLocalConfig( + aeiCaptureEnabled = true + ), + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + memoryServiceEnabled = true, + powerSaveModeServiceEnabled = true, + networkConnectivityServiceEnabled = true, + anrServiceEnabled = true, + uiLoadPerfTracingDisabled = false, + uiLoadPerfTracingSelectedOnly = false, + ), + backgroundActivityConfig = BackgroundActivityLocalConfig( + backgroundActivityCaptureEnabled = true + ), + captureFcmPiiData = true, + composeConfig = ComposeLocalConfig( + captureComposeOnClick = true + ), + crashHandler = CrashHandlerLocalConfig( + enabled = true + ), + networking = NetworkLocalConfig( + captureRequestContentLength = true, + enableNativeMonitoring = true, + enableNetworkSpanForwarding = true + ), + sigHandlerDetection = true, + taps = TapsLocalConfig( + captureCoordinates = true + ), + webViewConfig = WebViewLocalConfig( + captureWebViews = true, + captureQueryParams = true + ), + viewConfig = ViewLocalConfig( + enableAutomaticActivityCapture = true + ) + ), + null + ) + ) + ) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `verify disabling ui load traces`() { + val instrumentation = createEnabledFeatureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + true, + SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + uiLoadPerfTracingDisabled = true, + ) + ), + null + ) + ) + ) + + listOf( + ConfigMethod("isUiLoadTracingEnabled", "()Z", false), + ConfigMethod("isUiLoadTracingTraceAll", "()Z", false), + ).forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `verify enabling only selected activities for ui load traces`() { + val instrumentation = createEnabledFeatureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + true, + SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + uiLoadPerfTracingSelectedOnly = true, + ) + ), + null + ) + ) + ) + + listOf( + ConfigMethod("isUiLoadTracingEnabled", "()Z", true), + ConfigMethod("isUiLoadTracingTraceAll", "()Z", false), + ).forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `verify disabling and enabling selected ui load traces end up disabling both`() { + val instrumentation = createEnabledFeatureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + true, + SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig( + uiLoadPerfTracingDisabled = true, + uiLoadPerfTracingSelectedOnly = true, + ) + ), + null + ) + ) + ) + + listOf( + ConfigMethod("isUiLoadTracingEnabled", "()Z", false), + ConfigMethod("isUiLoadTracingTraceAll", "()Z", false), + ).forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `verify not specifying any configuration for ui load traces`() { + val instrumentation = createEnabledFeatureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + true, + SdkLocalConfig( + automaticDataCaptureConfig = AutomaticDataCaptureLocalConfig() + ), + null + ) + ) + ) + + listOf( + ConfigMethod("isUiLoadTracingEnabled", "()Z", true), + ConfigMethod("isUiLoadTracingTraceAll", "()Z", true), + ).forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..79abdae06b --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/NetworkCaptureConfigInstrumentationKtTest.kt @@ -0,0 +1,64 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.DomainLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.NetworkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.junit.Test + +class NetworkCaptureConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null + ) + + private val methods = listOf( + ConfigMethod("getRequestLimitPerDomain", "()I", 567), + ConfigMethod("getIgnoredRequestPatternList", "()Ljava/util/List;", listOf("pattern1", "pattern2")), + ConfigMethod("getNetworkBodyCapturePublicKey", "()Ljava/lang/String;", "my_key"), + ConfigMethod("getLimitsByDomain", "()Ljava/util/Map;", mapOf("domain1" to "1", "domain2" to "2")), + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createNetworkCaptureConfigInstrumentation(cfg) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = + createNetworkCaptureConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + null, + SdkLocalConfig( + capturePublicKey = "my_key", + networking = NetworkLocalConfig( + defaultCaptureLimit = 567, + disabledUrlPatterns = listOf("pattern1", "pattern2"), + domains = listOf( + DomainLocalConfig("domain1", 1), + DomainLocalConfig("domain2", 2) + ) + ) + ), + null + ) + ) + ) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..f5a1e192ca --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/ProjectConfigInstrumentationKtTest.kt @@ -0,0 +1,65 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.junit.Test + +class ProjectConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null, + embraceConfig = EmbraceVariantConfig( + "", + "", + null, + null, + null, + null + + ) + ) + + private val methods = listOf( + ConfigMethod("getAppFramework", "()Ljava/lang/String;", "native"), + ConfigMethod("getBuildId", "()Ljava/lang/String;", "my_id"), + ConfigMethod("getBuildType", "()Ljava/lang/String;", "build_type"), + ConfigMethod("getBuildFlavor", "()Ljava/lang/String;", "build_flavor") + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createProjectConfigInstrumentation(cfg) + verifyConfigMethodVisitor(instrumentation, ConfigMethod("getAppId", "()Ljava/lang/String;", "")) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = createProjectConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + appId = "abcde", + apiToken = null, + ndkEnabled = null, + sdkConfig = SdkLocalConfig(appFramework = "native"), + unityConfig = null + ), + buildId = "my_id", + buildType = "build_type", + buildFlavor = "build_flavor" + ) + ) + verifyConfigMethodVisitor(instrumentation, ConfigMethod("getAppId", "()Ljava/lang/String;", "abcde")) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..ffefb64fe1 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/RedactionConfigInstrumentationKtTest.kt @@ -0,0 +1,50 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.junit.Test + +class RedactionConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null + ) + + private val methods = listOf( + ConfigMethod("getSensitiveKeysDenylist", "()Ljava/util/List;", listOf("password")) + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createRedactionConfigInstrumentation(cfg) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = createRedactionConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + null, + SdkLocalConfig( + sensitiveKeysDenylist = listOf("password") + ), + null + ) + ) + ) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentationKtTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentationKtTest.kt new file mode 100644 index 0000000000..7bccd256ac --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/config/arch/sdk/SessionConfigInstrumentationKtTest.kt @@ -0,0 +1,55 @@ +package io.embrace.android.gradle.plugin.instrumentation.config.arch.sdk + +import io.embrace.android.gradle.plugin.instrumentation.config.model.EmbraceVariantConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SdkLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.SessionLocalConfig +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import org.junit.Test + +class SessionConfigInstrumentationKtTest { + + private val cfg = VariantConfig( + "", + null, + null, + null, + null + ) + + private val methods = listOf( + ConfigMethod("getSessionComponents", "()Ljava/util/List;", listOf("component")), + ConfigMethod("getFullSessionEvents", "()Ljava/util/List;", listOf("event")) + ) + + @Test + fun `test empty cfg`() { + val instrumentation = createSessionConfigInstrumentation(cfg) + + methods.map { it.copy(result = null) }.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } + + @Test + fun `test instrumentation`() { + val instrumentation = createSessionConfigInstrumentation( + cfg.copy( + embraceConfig = EmbraceVariantConfig( + null, + null, + null, + SdkLocalConfig( + sessionConfig = SessionLocalConfig( + sessionComponents = listOf("component"), + fullSessionEvents = listOf("event") + ) + ), + null + ) + ) + ) + methods.forEach { method -> + verifyConfigMethodVisitor(instrumentation, method) + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt new file mode 100644 index 0000000000..a5faae9add --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestBytecodeInstrumentationParams.kt @@ -0,0 +1,48 @@ +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import io.embrace.android.gradle.plugin.instrumentation.BytecodeInstrumentationParams +import io.embrace.android.gradle.plugin.instrumentation.ClassInstrumentationFilter +import io.embrace.android.gradle.plugin.instrumentation.config.model.VariantConfig +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import org.gradle.api.internal.provider.DefaultProperty +import org.gradle.api.internal.provider.PropertyHost +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.Property + +class TestBytecodeInstrumentationParams( + logLevel: LogLevel? = null, + disabled: Boolean = false, + classInstrumentationFilter: ClassInstrumentationFilter = ClassInstrumentationFilter(emptyList()), + invalidate: Long = -1, + instrumentFirebaseMessaging: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_FIREBASE_MESSAGING, + instrumentWebview: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_WEBVIEW, + instrumentOkHttp: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_OKHTTP, + instrumentOnLongClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_LONG_CLICK, + instrumentOnClick: Boolean = SwazzlerExtension.DEFAULT_INSTRUMENT_ON_CLICK +) : BytecodeInstrumentationParams { + + override val config: Property = + DefaultProperty(PropertyHost.NO_OP, VariantConfig::class.javaObjectType).convention( + VariantConfig("", "", null, null, null, null) + ) + override val logLevel: Property = + DefaultProperty(PropertyHost.NO_OP, LogLevel::class.javaObjectType).convention(logLevel) + override val disabled: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(disabled) + override val classInstrumentationFilter: Property = + DefaultProperty(PropertyHost.NO_OP, ClassInstrumentationFilter::class.javaObjectType).convention( + classInstrumentationFilter + ) + override val invalidate: Property = + DefaultProperty(PropertyHost.NO_OP, Long::class.javaObjectType).convention(invalidate) + override val shouldInstrumentFirebaseMessaging: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentFirebaseMessaging) + override val shouldInstrumentWebview: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentWebview) + override val shouldInstrumentOkHttp: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOkHttp) + override val shouldInstrumentOnLongClick: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnLongClick) + override val shouldInstrumentOnClick: Property = + DefaultProperty(PropertyHost.NO_OP, Boolean::class.javaObjectType).convention(instrumentOnClick) +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassContext.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassContext.kt new file mode 100644 index 0000000000..617a3e28c6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassContext.kt @@ -0,0 +1,11 @@ +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import com.android.build.api.instrumentation.ClassContext +import com.android.build.api.instrumentation.ClassData + +class TestClassContext(override val currentClassData: ClassData) : ClassContext { + + override fun loadClassData(className: String): ClassData? { + error("Classloading not implemented") + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassData.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassData.kt new file mode 100644 index 0000000000..e3780aaee0 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassData.kt @@ -0,0 +1,10 @@ +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import com.android.build.api.instrumentation.ClassData + +class TestClassData( + override val className: String, + override val classAnnotations: List = emptyList(), + override val interfaces: List = emptyList(), + override val superClasses: List = emptyList() +) : ClassData diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassVisitor.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassVisitor.kt new file mode 100644 index 0000000000..55b87ac73e --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestClassVisitor.kt @@ -0,0 +1,6 @@ +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.objectweb.asm.ClassVisitor + +class TestClassVisitor(api: Int = ASM_API_VERSION) : ClassVisitor(api) diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestInstrumentationContext.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestInstrumentationContext.kt new file mode 100644 index 0000000000..2e13a1d7bd --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestInstrumentationContext.kt @@ -0,0 +1,14 @@ +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import com.android.build.api.instrumentation.InstrumentationContext +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.gradle.api.internal.provider.DefaultProperty +import org.gradle.api.internal.provider.PropertyHost +import org.gradle.api.provider.Property + +@Suppress("UnstableApiUsage") +class TestInstrumentationContext : InstrumentationContext { + + override val apiVersion: Property = + DefaultProperty(PropertyHost.NO_OP, Int::class.javaObjectType).convention(ASM_API_VERSION) +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestVisitorFactoryImpl.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestVisitorFactoryImpl.kt new file mode 100644 index 0000000000..61feba58aa --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/fakes/TestVisitorFactoryImpl.kt @@ -0,0 +1,18 @@ +@file:Suppress("UnstableApiUsage") + +package io.embrace.android.gradle.plugin.instrumentation.fakes + +import com.android.build.api.instrumentation.InstrumentationContext +import io.embrace.android.gradle.plugin.instrumentation.BytecodeInstrumentationParams +import io.embrace.android.gradle.plugin.instrumentation.EmbraceClassVisitorFactory +import org.gradle.api.internal.provider.DefaultProperty +import org.gradle.api.internal.provider.PropertyHost +import org.gradle.api.provider.Property + +internal class TestVisitorFactoryImpl( + override val instrumentationContext: InstrumentationContext = TestInstrumentationContext(), + params: BytecodeInstrumentationParams = TestBytecodeInstrumentationParams() +) : EmbraceClassVisitorFactory() { + override val parameters: Property = + DefaultProperty(PropertyHost.NO_OP, BytecodeInstrumentationParams::class.javaObjectType).convention(params) +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapterTest.kt new file mode 100644 index 0000000000..2dca27dd4e --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpClassAdapterTest.kt @@ -0,0 +1,51 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class OkHttpClassAdapterTest { + + private val adapter = OkHttpClassAdapter(ASM_API_VERSION, null) {} + + @Test + fun testCtorVisited() { + val visitor = adapter.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, emptyArray()) + assertFalse(visitor is OkHttpMethodAdapter) + } + + @Test + fun testBuildVisited() { + val visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "build", + "()Lokhttp3/OkHttpClient;", + null, + emptyArray() + ) + assertTrue(visitor is OkHttpMethodAdapter) + } + + @Test + fun testMethodNotVisited() { + var visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "foo", + "()Lokhttp3/OkHttpClient;", + null, + emptyArray() + ) + assertFalse(visitor is OkHttpMethodAdapter) + + visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "build", + "()Lokhttp3/OkHttpClient\$Builder;", + null, + emptyArray() + ) + assertFalse(visitor is OkHttpMethodAdapter) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..1476a14a59 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OkHttpConfigMethodAdapterTest.kt @@ -0,0 +1,29 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class OkHttpConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + OkHttpMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/okhttp3/swazzle/callback/okhttp3/OkHttpClient\$Builder", + "_preBuild", + "(Lokhttp3/OkHttpClient\$Builder;)V", + false + ) + visitCode() + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapterTest.kt new file mode 100644 index 0000000000..0104a2b9e6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickClassAdapterTest.kt @@ -0,0 +1,47 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class OnClickClassAdapterTest { + + private val adapter = OnClickClassAdapter(ASM_API_VERSION, null) {} + + @Test + fun testOnClickVisited() { + val visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onClick", + "(Landroid/view/View;)V", + null, + emptyArray() + ) + assertTrue(visitor is OnClickMethodAdapter) + } + + @Test + fun testStaticOnClickVisited() { + val access = Opcodes.ACC_STATIC.plus(Opcodes.ACC_SYNTHETIC) + val visitor = + adapter.visitMethod(access, "onClick", "(Landroid/view/View;)V", null, emptyArray()) + assertTrue(visitor is OnClickStaticMethodAdapter) + } + + @Test + fun testMethodNotVisited() { + var visitor = adapter.visitMethod(Opcodes.ACC_PUBLIC, "onClick", "()V", null, emptyArray()) + assertFalse(visitor is OnClickMethodAdapter) + + visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "foo", + "(Landroid/view/View;)V", + null, + emptyArray() + ) + assertFalse(visitor is OnClickMethodAdapter) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..9dd6a7608e --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickConfigMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class OnClickConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + OnClickMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitVarInsn(Opcodes.ALOAD, 0) + visitVarInsn(Opcodes.ALOAD, 1) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnClickListener", + "_preOnClick", + "(Landroid/view/View\$OnClickListener;Landroid/view/View;)V", + false + ) + visitCode() + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..35163cec65 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnClickStaticConfigMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class OnClickStaticConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + OnClickStaticMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitInsn(Opcodes.ACONST_NULL) + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnClickListener", + "_preOnClick", + "(Landroid/view/View\$OnClickListener;Landroid/view/View;)V", + false + ) + visitCode() + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapterTest.kt new file mode 100644 index 0000000000..e77d961808 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickClassAdapterTest.kt @@ -0,0 +1,48 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class OnLongClickClassAdapterTest { + + private val adapter = OnLongClickClassAdapter(ASM_API_VERSION, null) {} + + @Test + fun testOnLongClickVisited() { + val visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onLongClick", + "(Landroid/view/View;)Z", + null, + emptyArray() + ) + assertTrue(visitor is OnLongClickMethodAdapter) + } + + @Test + fun testStaticOnLongClickVisited() { + val access = Opcodes.ACC_STATIC.plus(Opcodes.ACC_SYNTHETIC) + val visitor = + adapter.visitMethod(access, "onLongClick", "(Landroid/view/View;)Z", null, emptyArray()) + assertTrue(visitor is OnLongClickStaticMethodAdapter) + } + + @Test + fun testMethodNotVisited() { + var visitor = + adapter.visitMethod(Opcodes.ACC_PUBLIC, "onLongClick", "()V", null, emptyArray()) + assertFalse(visitor is OnLongClickMethodAdapter) + + visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "foo", + "(Landroid/view/View;)Z", + null, + emptyArray() + ) + assertFalse(visitor is OnLongClickMethodAdapter) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..1392360173 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickConfigMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class OnLongClickConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + OnLongClickMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitVarInsn(Opcodes.ALOAD, 0) + visitVarInsn(Opcodes.ALOAD, 1) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnLongClickListener", + "_preOnLongClick", + "(Landroid/view/View\$OnLongClickListener;Landroid/view/View;)V", + false + ) + visitCode() + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..75d814367c --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/OnLongClickStaticConfigMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class OnLongClickStaticConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + OnLongClickStaticMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitInsn(Opcodes.ACONST_NULL) + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/ViewSwazzledHooks\$OnLongClickListener", + "_preOnLongClick", + "(Landroid/view/View\$OnLongClickListener;Landroid/view/View;)V", + false + ) + visitCode() + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapterTest.kt new file mode 100644 index 0000000000..4d4a2a6996 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientClassAdapterTest.kt @@ -0,0 +1,45 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.embrace.android.gradle.plugin.instrumentation.ASM_API_VERSION +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.objectweb.asm.Opcodes + +class WebViewClientClassAdapterTest { + + private val adapter = WebViewClientClassAdapter(ASM_API_VERSION, null) {} + + @Test + fun testOnPageStartedVisited() { + val visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + null, + emptyArray() + ) + assertTrue(visitor is WebViewClientMethodAdapter) + } + + @Test + fun testMethodNotVisited() { + var visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/Boolean;Landroid/graphics/Bitmap;)V", + null, + emptyArray() + ) + assertFalse(visitor is WebViewClientMethodAdapter) + + visitor = adapter.visitMethod( + Opcodes.ACC_PUBLIC, + "onPageStart", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + null, + emptyArray() + ) + assertFalse(visitor is WebViewClientMethodAdapter) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapterTest.kt new file mode 100644 index 0000000000..c532d0ddc8 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class WebViewClientMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + WebViewClientMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitVarInsn(Opcodes.ALOAD, 1) + visitVarInsn(Opcodes.ALOAD, 2) + visitVarInsn(Opcodes.ALOAD, 3) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/WebViewClientSwazzledHooks", + "_preOnPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + false + ) + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideConfigMethodAdapterTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideConfigMethodAdapterTest.kt new file mode 100644 index 0000000000..78abae800c --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/instrumentation/visitor/WebViewClientOverrideConfigMethodAdapterTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.instrumentation.visitor + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +class WebViewClientOverrideConfigMethodAdapterTest { + + @Test + fun visitCode() { + val visitor = mockk(relaxed = true) + WebViewClientOverrideMethodAdapter(Opcodes.ASM6, visitor).visitCode() + verify(exactly = 1) { + with(visitor) { + visitVarInsn(Opcodes.ALOAD, 1) + visitVarInsn(Opcodes.ALOAD, 2) + visitVarInsn(Opcodes.ALOAD, 3) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "io/embrace/android/embracesdk/WebViewClientSwazzledHooks", + "_preOnPageStarted", + "(Landroid/webkit/WebView;Ljava/lang/String;Landroid/graphics/Bitmap;)V", + false + ) + } + } + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadHandshakeTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadHandshakeTest.kt new file mode 100644 index 0000000000..0a74e1ec4e --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadHandshakeTest.kt @@ -0,0 +1,144 @@ +package io.embrace.android.gradle.plugin.ndk + +import io.embrace.android.gradle.plugin.network.NetworkService +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshake +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeRequest +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadHandshakeResponse +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class NdkUploadHandshakeTest { + + private lateinit var mockNetworkService: NetworkService + private lateinit var ndkUploadHandshakeRequest: NdkUploadHandshakeRequest + private lateinit var ndkUploadHandshake: NdkUploadHandshake + + @Before + fun setUp() { + mockNetworkService = mockk() + ndkUploadHandshake = NdkUploadHandshake( + networkService = mockNetworkService + ) + + ndkUploadHandshakeRequest = NdkUploadHandshakeRequest( + appId = APP_ID, + apiToken = API_TOKEN, + variant = VARIANT, + archSymbols = emptyMap() + ) + } + + @Test + fun `NdkUploadHandshakeRequest serialization test - empty archs`() { + val request = NdkUploadHandshakeRequest( + appId = APP_ID, + apiToken = API_TOKEN, + variant = VARIANT, + archSymbols = emptyMap() + ) + val expectedRequest = """ + { + "app": "$APP_ID", + "token": "$API_TOKEN", + "variant": "$VARIANT", + "archs": {} + } + """.trim().replace("\\s".toRegex(), "") + val serializedRequest = MoshiSerializer().toJson(request) + assertEquals(expectedRequest, serializedRequest) + } + + @Test + fun `NdkUploadHandshakeRequest serialization test - with symbols`() { + val request = NdkUploadHandshakeRequest( + appId = APP_ID, + apiToken = API_TOKEN, + variant = VARIANT, + archSymbols = mapOf( + "x86_64" to mapOf("libnative.so" to "f067beecae4f901c81c642d002810944460efd7b"), + "x86" to mapOf("libnative.so" to "3c84ca4cf150a346db8d195426e520b7a45a0118"), + "armeabi-v7a" to mapOf("libnative.so" to "b621b4bac764b4a1d6166984d63d9958187439a6"), + "arm64-v8a" to mapOf("libnative.so" to "7d8c51cd16d00a369a1b923e1e9aed88c501beee") + ) + ) + val expectedRequest = """ + { + "app": "$APP_ID", + "token": "$API_TOKEN", + "variant": "$VARIANT", + "archs": { + "x86_64": { + "libnative.so": "f067beecae4f901c81c642d002810944460efd7b" + }, + "x86": { + "libnative.so": "3c84ca4cf150a346db8d195426e520b7a45a0118" + }, + "armeabi-v7a": { + "libnative.so": "b621b4bac764b4a1d6166984d63d9958187439a6" + }, + "arm64-v8a": { + "libnative.so": "7d8c51cd16d00a369a1b923e1e9aed88c501beee" + } + } + } + """.trim().replace("\\s".toRegex(), "") + val serializedRequest = MoshiSerializer().toJson(request) + assertEquals(expectedRequest, serializedRequest) + } + + @Test + fun `NdkUploadHandshakeResponse deserialization test - empty symbols`() { + val response = """ + { + "archs": {} + } + """.trim().replace("\\s".toRegex(), "") + val deserializedResponse = + MoshiSerializer().fromJson(response, NdkUploadHandshakeResponse::class.java) + assertNotNull(deserializedResponse.symbols) + assertTrue(deserializedResponse.symbols!!.isEmpty()) + } + + @Test + fun `NdkUploadHandshakeResponse deserialization test - with symbols`() { + val response = """ + { + "archs": { + "arm64-v8a": [ + "libnative.so" + ], + "armeabi-v7a": [ + "libnative.so" + ], + "x86": [ + "libnative.so" + ], + "x86_64": [ + "libnative.so" + ] + } + } + """.replace("\\s".toRegex(), "") + + val deserializedResponse = + MoshiSerializer().fromJson(response, NdkUploadHandshakeResponse::class.java) + val expectedResponse = NdkUploadHandshakeResponse( + symbols = mapOf( + "x86" to listOf("libnative.so"), + "x86_64" to listOf("libnative.so"), + "armeabi-v7a" to listOf("libnative.so"), + "arm64-v8a" to listOf("libnative.so") + ) + ) + assertEquals(expectedResponse, deserializedResponse) + } +} + +private const val APP_ID = "appId" +private const val API_TOKEN = "apiToken" +private const val VARIANT = "variant" diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadTaskRegistrationTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadTaskRegistrationTest.kt new file mode 100644 index 0000000000..489e37c6fd --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/NdkUploadTaskRegistrationTest.kt @@ -0,0 +1,337 @@ +package io.embrace.android.gradle.plugin.ndk + +import io.embrace.android.gradle.plugin.EXTENSION_NAME +import io.embrace.android.gradle.plugin.config.ProjectType +import io.embrace.android.gradle.plugin.config.UnitySymbolsDir +import io.embrace.android.gradle.plugin.extension.EXTENSION_EMBRACE_INTERNAL +import io.embrace.android.gradle.plugin.extension.EmbraceExtensionInternal +import io.embrace.android.gradle.plugin.gradle.isTaskRegistered +import io.embrace.android.gradle.plugin.model.AndroidCompactedVariantData +import io.embrace.android.gradle.plugin.network.EmbraceEndpoint +import io.embrace.android.gradle.plugin.tasks.common.RequestParams +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadTask +import io.embrace.android.gradle.plugin.tasks.ndk.NdkUploadTaskRegistration +import io.embrace.android.gradle.plugin.tasks.registration.RegistrationParams +import io.embrace.android.gradle.plugin.util.capitalizedString +import io.embrace.android.gradle.swazzler.plugin.extension.SwazzlerExtension +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.tasks.TaskProvider +import org.gradle.testfixtures.ProjectBuilder +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class NdkUploadTaskRegistrationTest { + + private val baseUrl = "https://example.com/api" + + companion object { + val unitySymbolsDir = mockk() + + @AfterClass + @JvmStatic + fun tearDown() { + unmockkAll() + } + } + + private fun createExtension(project: Project, ndkEnabled: Boolean, projectType: ProjectType): EmbraceExtensionInternal { + val extension = project.extensions.create( + EXTENSION_EMBRACE_INTERNAL, + EmbraceExtensionInternal::class.java, + project.objects + ) + + project.extensions.create( + EXTENSION_NAME, + SwazzlerExtension::class.java, + project.objects + ) + + extension.variants.create("variantName").also { newVariant -> + newVariant.appId.set("appId") + newVariant.apiToken.set("apiToken") + newVariant.ndkEnabled.set(ndkEnabled) + newVariant.projectType.set(projectType) + newVariant.unitySymbolsDir.set(unitySymbolsDir) + } + + return extension + } + + private fun registerTestTask(project: Project, taskName: String): TaskProvider = + project.tasks.register( + taskName, + DefaultTask::class.java + ) + + @Test + fun `test configure ndkUploadTask with ndk disabled`() { + val taskName = NdkUploadTask.NAME + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + val project = ProjectBuilder.builder().build() + + val extension = createExtension(project, false, ProjectType.UNITY) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + assertFalse( + project.isTaskRegistered( + taskName, + variant.name + ) + ) + registration.register(params) + + val ndkUploadTask = project.tasks.findByName("$taskName${variant.name.capitalizedString()}") as? NdkUploadTask + assertNull(ndkUploadTask) + } + + @Test + fun `test configure ndkUploadTask for native project type`() { + val taskName = NdkUploadTask.NAME + val project = ProjectBuilder.builder().build() + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + + val extension = createExtension(project, true, ProjectType.NATIVE) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + assertFalse( + project.isTaskRegistered( + taskName, + variant.name + ) + ) + registration.register(params) + + assertTrue( + project.isTaskRegistered( + NdkUploadTask.NAME, + variant.name + ) + ) + } + + @Test + fun `test configure ndkUploadTask for unity 2018-2019 project type`() { + val taskName = NdkUploadTask.NAME + val project = ProjectBuilder.builder().build() + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + val mergeJniLibFoldersTaskName = + "merge${variant.name.capitalizedString()}JniLibFolders" + + registerTestTask(project, mergeJniLibFoldersTaskName) + + val transformNativeLibsTaskName = + "transformNativeLibsWithMergeJniLibsFor${variant.name.capitalizedString()}" + + val extension = createExtension(project, true, ProjectType.UNITY) + + registerTestTask(project, transformNativeLibsTaskName) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + assertFalse( + project.isTaskRegistered( + taskName, + variant.name + ) + ) + registration.register(params) + + val ndkUploadTask: NdkUploadTask = + project.tasks.findByName("$taskName${variant.name.capitalizedString()}") as NdkUploadTask + + assertTrue( + project.isTaskRegistered( + NdkUploadTask.NAME, + variant.name + ) + ) + assertTrue( + ndkUploadTask.mustRunAfter.getDependencies(ndkUploadTask).toString() + .contains(mergeJniLibFoldersTaskName) + ) + } + + @Test + fun `test configure ndkUploadTask for unity 2020 project type`() { + val taskName = NdkUploadTask.NAME + val project = ProjectBuilder.builder().build() + + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + val mergeJniLibFoldersTaskName = + "merge${variant.name.capitalizedString()}JniLibFolders" + + registerTestTask(project, mergeJniLibFoldersTaskName) + + val mergeNativeLibs = + "merge${variant.name.capitalizedString()}NativeLibs" + + registerTestTask(project, mergeNativeLibs) + val extension = createExtension(project, true, ProjectType.UNITY) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + + assertFalse( + project.isTaskRegistered( + taskName, + variant.name + ) + ) + registration.register(params) + + val ndkUploadTask: NdkUploadTask = + project.tasks.findByName("$taskName${variant.name.capitalizedString()}") as NdkUploadTask + + assertTrue( + project.isTaskRegistered( + NdkUploadTask.NAME, + variant.name + ) + ) + assertTrue( + ndkUploadTask.mustRunAfter.getDependencies(ndkUploadTask).toString() + .contains(mergeNativeLibs) + ) + } + + @Test + fun `test execute throws the exception`() { + val project = ProjectBuilder.builder().build() + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + val extension = createExtension(project, true, ProjectType.NATIVE) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + try { + registration.register(params) + } catch (e: Exception) { + assertTrue(e is NullPointerException) + } + } + + @Test + fun `verify Ndk upload task configuration once is registered`() { + val taskName = NdkUploadTask.NAME + val project = ProjectBuilder.builder().build() + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + + val extension = createExtension(project, true, ProjectType.NATIVE) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + registration.register(params) + val ndkUploadTask: NdkUploadTask = + project.tasks.findByName("$taskName${variant.name.capitalizedString()}") as NdkUploadTask + + assertEquals( + ndkUploadTask.requestParams.get(), + RequestParams( + appId = "appId", + apiToken = "apiToken", + endpoint = EmbraceEndpoint.NDK, + baseUrl, + ) + ) + assertEquals(ndkUploadTask.unitySymbolsDir.orNull, null) + } + + @Test + fun `verify Ndk upload task configuration once is registered for unity`() { + val taskName = NdkUploadTask.NAME + val project = ProjectBuilder.builder().build() + val variant = mockk(relaxed = true) { + every { name } returns "variantName" + } + + val extension = createExtension(project, true, ProjectType.UNITY) + + val registration = NdkUploadTaskRegistration(mockk(relaxed = true)) + val params = RegistrationParams( + project, + mockk(relaxed = true), + variant, + mockk(relaxed = true), + extension, + baseUrl, + ) + registration.register(params) + val ndkUploadTask: NdkUploadTask = + project.tasks.findByName("$taskName${variant.name.capitalizedString()}") as NdkUploadTask + + assertEquals( + ndkUploadTask.requestParams.get(), + RequestParams( + appId = "appId", + apiToken = "apiToken", + endpoint = EmbraceEndpoint.NDK, + baseUrl, + ) + ) + assertEquals(ndkUploadTask.unitySymbolsDir.orNull, unitySymbolsDir) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/SymbolResourceInjectorTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/SymbolResourceInjectorTest.kt new file mode 100644 index 0000000000..b6b3a40d9d --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/ndk/SymbolResourceInjectorTest.kt @@ -0,0 +1,30 @@ +package io.embrace.android.gradle.plugin.ndk + +import io.embrace.android.gradle.ResourceReader +import io.embrace.android.gradle.plugin.tasks.ndk.SymbolResourceInjector +import io.embrace.android.gradle.plugin.util.serialization.MoshiSerializer +import org.junit.Assert.assertEquals +import org.junit.Test +import java.nio.file.Files + +class SymbolResourceInjectorTest { + + private val injector = SymbolResourceInjector(MoshiSerializer()) + + @Test + fun buildSymbolResourceValue() { + val file = Files.createTempFile("test", "xml").toFile() + injector.writeSymbolResourceFile( + file, + mapOf( + "armeabi-v8a" to mapOf( + "libmygame.so" to "F0980ACB19823" + ) + ) + ) + + val output = file.readText() + val expected = ResourceReader.readResourceAsText("injected_symbol_resources") + assertEquals(expected, output) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/reactnative/RnFilesFinderTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/reactnative/RnFilesFinderTest.kt new file mode 100644 index 0000000000..a08904ba13 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/reactnative/RnFilesFinderTest.kt @@ -0,0 +1,67 @@ +package io.embrace.android.gradle.plugin.reactnative + +import io.embrace.android.gradle.plugin.Logger +import io.embrace.android.gradle.plugin.tasks.reactnative.RnFilesFinder +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File + +class RnFilesFinderTest { + + private val mockLogger = mockk>(relaxed = true) + + private val variantName = "demoRelease" + private val buildDir = File("build") + private val bundleDirGeneric = File(buildDir, "generated/assets/react/$variantName") + .also { it.mkdirs() } + + @Test + fun `getReactNativeSourcemapFilePath - custom bundle asset name`() { + val reactProps = mapOf( + "bundleAssetName" to "my-game.bundle" + ) + + val finder = RnFilesFinder(reactProps, File(""), mockLogger) + val observed = finder.getReactNativeSourcemapFilePath("myFlavor") + + val expected = "generated/sourcemaps/react/myFlavor/my-game.bundle.map" + assertEquals(expected, observed) + } + + @Test + fun `getReactNativeSourcemapFilePath - default bundle asset name`() { + val reactProps = null + + val finder = RnFilesFinder(reactProps, buildDir, mockLogger) + val observed = finder.getReactNativeSourcemapFilePath("myFlavor") + + val expected = "generated/sourcemaps/react/myFlavor/index.android.bundle.map" + assertEquals(expected, observed) + } + + @Test + fun `getEmbraceSourcemapFilePath - log deprecated commands if present`() { + val reactProps = mapOf( + "extraPackagerArgs" to listOf("a --sourcemap-output", "b") + ) + + val finder = RnFilesFinder(reactProps, buildDir, mockLogger) + finder.getEmbraceSourcemapFilePath() + + verify { mockLogger.warn(any()) } + } + + @Test + fun `getBundleFile - default bundleAssetDirectory (from task), default bundleAssetName`() { + val bundleFile = File(bundleDirGeneric, "index.android.bundle") + bundleFile.createNewFile() + + val finder = RnFilesFinder(null, buildDir, mockLogger) + val observed = finder.getBundleFile() + + val expectedPath = bundleFile.path + assertEquals(expectedPath, observed?.path) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpUtilsTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpUtilsTest.kt new file mode 100644 index 0000000000..2d18d0f494 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpUtilsTest.kt @@ -0,0 +1,36 @@ +package io.embrace.android.gradle.plugin.util + +import io.embrace.android.gradle.plugin.agp.AgpUtils +import io.mockk.every +import io.mockk.mockk +import org.gradle.api.Task +import org.gradle.api.tasks.TaskProvider +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AgpUtilsTest { + + @Test + fun `desugaring should not be required for min sdk level greater than 23`() { + assertFalse(AgpUtils.isDesugaringRequired(24)) + } + + @Test + fun `desugaring should be required for min sdk level less than 24`() { + assertTrue(AgpUtils.isDesugaringRequired(23)) + } + + @Test + fun `verify task is dexguard`() { + val task = mockk> { + every { name } returns "dexguardApkDebug" + } + + assertTrue(AgpUtils.isDexguard(task)) + + every { task.name } returns "transformClassesAndResourcesWithProguardForDebug" + + assertFalse(AgpUtils.isDexguard(task)) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpVersionTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpVersionTest.kt new file mode 100644 index 0000000000..3cf7116b66 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/AgpVersionTest.kt @@ -0,0 +1,16 @@ +package io.embrace.android.gradle.plugin.util + +import io.embrace.android.gradle.plugin.agp.AgpVersion.AGP_8_0_0 +import io.embrace.android.gradle.plugin.agp.AgpVersion.AGP_8_3_0 +import org.junit.Assert.assertEquals +import org.junit.Test + +class AgpVersionTest { + + @Test + fun testComparator() { + assertEquals(0, AGP_8_0_0.compareTo(AGP_8_0_0)) + assertEquals(1, AGP_8_3_0.compareTo(AGP_8_0_0)) + assertEquals(-1, AGP_8_0_0.compareTo(AGP_8_3_0)) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/GradleVersionTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/GradleVersionTest.kt new file mode 100644 index 0000000000..8b5b5345a2 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/GradleVersionTest.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.util + +import io.embrace.android.gradle.plugin.gradle.GradleVersion +import org.junit.Assert.assertEquals +import org.junit.Test + +class GradleVersionTest { + + @Test + fun testCompileVersion() { + assertEquals("8.12.1", GradleVersion.CURRENT.toString()) + } + + @Test + fun testComparator() { + assertEquals(0, GradleVersion.GRADLE_8_0.compareTo(GradleVersion.GRADLE_8_0)) + assertEquals(1, GradleVersion.CURRENT.compareTo(GradleVersion.GRADLE_8_0)) + assertEquals(-1, GradleVersion.GRADLE_8_0.compareTo(GradleVersion.CURRENT)) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/HashUtilsTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/HashUtilsTest.kt new file mode 100644 index 0000000000..74adbe841e --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/HashUtilsTest.kt @@ -0,0 +1,20 @@ +package io.embrace.android.gradle.plugin.util + +import io.embrace.android.gradle.plugin.hash.calculateSha1ForFile +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File + +class HashUtilsTest { + + @Test + fun generateSha1Hash() { + val file = File.createTempFile("test", "txt").apply { + writeText("test") + } + assertEquals( + "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", + calculateSha1ForFile(file) + ) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/TestClock.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/TestClock.kt new file mode 100644 index 0000000000..3445054255 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/TestClock.kt @@ -0,0 +1,24 @@ +package io.embrace.android.gradle.plugin.util + +import io.embrace.android.gradle.plugin.system.Clock + +class TestClock : Clock { + private var currentTime: Long = 0 + + fun setCurrentTime(currentTime: Long) { + this.currentTime = currentTime + } + + fun tickSecond() { + tick(1000) + } + + @JvmOverloads + fun tick(millis: Long = 1) { + currentTime += millis + } + + override fun now(): Long { + return currentTime + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/UuidUtilsTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/UuidUtilsTest.kt new file mode 100644 index 0000000000..8f1e1aaf89 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/UuidUtilsTest.kt @@ -0,0 +1,15 @@ +package io.embrace.android.gradle.plugin.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.UUID + +class UuidUtilsTest { + + @Test + fun generateEmbraceUuid() { + val uuid = UUID.fromString("20bd98bc-af93-4b17-98e1-baf14a635bf4") + val output = UuidUtils.generateEmbraceUuid(uuid) + assertEquals("20BD98BCAF934B1798E1BAF14A635BF4", output) + } +} diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressorTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressorTest.kt new file mode 100644 index 0000000000..f7a60587c8 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/ZstdFileCompressorTest.kt @@ -0,0 +1,49 @@ +package io.embrace.android.gradle.plugin.util.compression + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import java.io.File + +class ZstdFileCompressorTest { + + private lateinit var project: Project + private lateinit var compressor: FileCompressor + + @Before + fun setup() { + project = ProjectBuilder.builder().build() + compressor = ZstdFileCompressor() + } + + @Test + fun `test compressFile returns a file with same content as compressed golden file`() { + val inputFile = File("$ASSETS_FILES_PATH/$MAPPING_FILE_UNCOMPRESSED_NAME") + + val file = File.createTempFile("temp", "temp") + val compressedFile: File? = compressor.compress(inputFile, file) + + val expectedFile = File("$ASSETS_FILES_PATH/$MAPPING_FILE_COMPRESSED_NAME") + checkNotNull(compressedFile) + assertEquals(expectedFile.readText(), compressedFile.readText()) + } + + @Test + fun `test compressFile returns null when input File is invalid`() { + val inputFile = File("imaginary-dir/$MAPPING_FILE_UNCOMPRESSED_NAME") + inputFile.setReadOnly() + + val file = File.createTempFile("temp", "temp") + val compressedFile: File? = compressor.compress(inputFile, file) + + assertNull(compressedFile) + } +} + +private const val ASSETS_FILES_PATH = + "src/test/java/io/embrace/android/gradle/plugin/util/compression/assets" +private const val MAPPING_FILE_UNCOMPRESSED_NAME = "mapping-file-uncompressed.txt" +private const val MAPPING_FILE_COMPRESSED_NAME = "mapping-file-compressed.txt" diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-compressed.txt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-compressed.txt new file mode 100644 index 0000000000..36c72a6634 Binary files /dev/null and b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-compressed.txt differ diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-uncompressed.txt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-uncompressed.txt new file mode 100644 index 0000000000..9a2afbbd8c --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/compression/assets/mapping-file-uncompressed.txt @@ -0,0 +1,23 @@ +# compiler: R8 +# compiler_version: 3.1.51 +# min_api: 21 +# pg_map_id: 9697d76 +# common_typos_disable +# {"id":"com.android.tools.r8.mapping","version":"1.0"} +android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer: + 1:1:void ():11:11 -> + 1:1:androidx.core.app.RemoteActionCompat read(androidx.versionedparcelable.VersionedParcel):13:13 -> read + 1:1:void write(androidx.core.app.RemoteActionCompat,androidx.versionedparcelable.VersionedParcel):17:17 -> write +android.support.v4.graphics.drawable.IconCompatParcelizer -> android.support.v4.graphics.drawable.IconCompatParcelizer: + 1:1:void ():11:11 -> + 1:1:androidx.core.graphics.drawable.IconCompat read(androidx.versionedparcelable.VersionedParcel):13:13 -> read + 1:1:void write(androidx.core.graphics.drawable.IconCompat,androidx.versionedparcelable.VersionedParcel):17:17 -> write +android.support.v4.media.MediaBrowserCompat$CustomActionResultReceiver -> android.support.v4.media.MediaBrowserCompat$CustomActionResultReceiver: + void onReceiveResult(int,android.os.Bundle) -> j +android.support.v4.media.MediaBrowserCompat$ItemReceiver -> android.support.v4.media.MediaBrowserCompat$ItemReceiver: + 1:1:void onReceiveResult(int,android.os.Bundle):2246:2246 -> j + 2:2:void onReceiveResult(int,android.os.Bundle):2248:2248 -> j + 3:4:void onReceiveResult(int,android.os.Bundle):2252:2253 -> j + 5:5:void onReceiveResult(int,android.os.Bundle):2256:2256 -> j + 6:6:void onReceiveResult(int,android.os.Bundle):2254:2254 -> j + 7:7:void onReceiveResult(int,android.os.Bundle):2249:2249 -> j \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializerTest.kt b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializerTest.kt new file mode 100644 index 0000000000..e402c55708 --- /dev/null +++ b/embrace-gradle-plugin/src/test/java/io/embrace/android/gradle/plugin/util/serialization/MoshiSerializerTest.kt @@ -0,0 +1,71 @@ +package io.embrace.android.gradle.plugin.util.serialization + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.junit.Assert.assertEquals +import org.junit.Test + +class MoshiSerializerTest { + private val moshiSerializer = MoshiSerializer() + + @Test + fun `toJson throws exception when serialization fails`() { + val data = Any() + try { + moshiSerializer.toJson(data) + } catch (e: IllegalArgumentException) { + assertEquals(IllegalArgumentException::class.java, e::class.java) + } + } + + @Test + fun `toJson throws exception when data is null`() { + try { + moshiSerializer.toJson(null) + } catch (e: IllegalArgumentException) { + assertEquals(IllegalArgumentException::class.java, e::class.java) + } + } + + @Test + fun `toJson returns JSON string representation of object`() { + val testObject = TestObject("Francisco", "Independiente") + val json = moshiSerializer.toJson(testObject) + val expectedJson = """{"name":"Francisco","team":"Independiente"}""" + assertEquals(expectedJson, json) + } + + @Test + fun `fromJson throws exception when deserialization fails`() { + val nonJsonString = "This is not a JSON string" + try { + moshiSerializer.fromJson(nonJsonString, TestObject::class.java) + } catch (e: IllegalArgumentException) { + assertEquals(IllegalArgumentException::class.java, e::class.java) + } + } + + @Test + fun `fromJson throws exception when json is empty`() { + val emptyJsonString = "" + try { + moshiSerializer.fromJson(emptyJsonString, TestObject::class.java) + } catch (e: IllegalArgumentException) { + assertEquals(IllegalArgumentException::class.java, e::class.java) + } + } + + @Test + fun `fromJson returns object of specified type`() { + val json = """{"name":"Francisco","team":"Independiente"}""" + val testObject = moshiSerializer.fromJson(json, TestObject::class.java) + assertEquals("Francisco", testObject.name) + assertEquals("Independiente", testObject.team) + } +} + +@JsonClass(generateAdapter = true) +class TestObject( + @Json(name = "name") val name: String, + @Json(name = "team") val team: String +) diff --git a/embrace-gradle-plugin/src/test/resources/build_info_file.xml b/embrace-gradle-plugin/src/test/resources/build_info_file.xml new file mode 100644 index 0000000000..5ea34dec98 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/build_info_file.xml @@ -0,0 +1,10 @@ + + + + sazwW + demo + 1B3C870C8AED4A21A2FCB170DF715978 + debug + false + eyJiYXNlX3VybHMiOnsiY29uZmlnLlJFTU9WRV9USElTX1RPX1NFVF9TVEFHSU5HIjoiaHR0cHM6Ly9hLXNhendXLmNvbmZpZy5zdGcuZW1iLWVuZy5jb20iLCJkYXRhLlJFTU9WRV9USElTX1RPX1NFVF9TVEFHSU5HIjoiaHR0cHM6Ly9hLXNhendXLmRhdGEuc3RnLmVtYi1lbmcuY29tIiwiZGF0YV9kZXYuUkVNT1ZFX1RISVNfVE9fU0VUX1NUQUdJTkciOiJodHRwczovL2Etc2F6d1cuZGF0YS5zdGcuZW1iLWVuZy5jb20iLCJjb25maWcuUkVNT1ZFX1RISVNfVE9fU0VUX01PQ0tfQVBJIjoiWU9VUl9MT0NBTF9JUF9IRVJFOjIzMDAyIiwiZGF0YS5SRU1PVkVfVEhJU19UT19TRVRfTU9DS19BUEkiOiJZT1VSX0xPQ0FMX0lQX0hFUkU6MjQwMDAiLCJkYXRhX2Rldi5SRU1PVkVfVEhJU19UT19TRVRfTU9DS19BUEkiOiJZT1VSX0xPQ0FMX0lQX0hFUkU6MjQwMDEifSwiYXBwIjp7InJlcG9ydF9kaXNrX3VzYWdlLlJFTU9WRV9USElTX1RPX1NFVCI6dHJ1ZX0sImNyYXNoX2hhbmRsZXIiOnsiZW5hYmxlZC5SRU1PVkVfVEhJU19UT19TRVQiOmZhbHNlfSwibmV0d29ya2luZyI6eyJjYXB0dXJlX3JlcXVlc3RfY29udGVudF9sZW5ndGguUkVNT1ZFX1RISVNfVE9fU0VUIjpmYWxzZSwiZGlzYWJsZWRfdXJsX3BhdHRlcm5zLlJFTU9WRV9USElTX1RPX1NFVCI6W10sImVuYWJsZV9uYXRpdmVfbW9uaXRvcmluZy5SRU1PVkVfVEhJU19UT19TRVQiOnRydWUsInRyYWNlX2lkX2hlYWRlci5SRU1PVkVfVEhJU19UT19TRVQiOiJ4LWVtYi10cmFjZS1pZCJ9LCJzZXNzaW9uIjp7ImFzeW5jX2VuZC5SRU1PVkVfVEhJU19UT19TRVQiOnRydWUsIm1heF9zZXNzaW9uX3NlY29uZHMuUkVNT1ZFX1RISVNfVE9fU0VUIjo2MH0sInN0YXJ0dXBfbW9tZW50Ijp7ImF1dG9tYXRpY2FsbHlfZW5kLlJFTU9WRV9USElTX1RPX1NFVCI6ZmFsc2UsInRha2Vfc2NyZWVuc2hvdC5SRU1PVkVfVEhJU19UT19TRVQiOmZhbHNlfSwidGFwcyI6eyJjYXB0dXJlX2Nvb3JkaW5hdGVzLlJFTU9WRV9USElTX1RPX1NFVCI6ZmFsc2V9LCJ3ZWJ2aWV3Ijp7ImNhcHR1cmVfcXVlcnlfcGFyYW1zLlJFTU9WRV9USElTX1RPX1NFVCI6dHJ1ZSwiZW5hYmxlLlJFTU9WRV9USElTX1RPX1NFVCI6dHJ1ZX0sImF1dG9tYXRpY19kYXRhX2NhcHR1cmUiOnsibWVtb3J5X2luZm8uUkVNT1ZFX1RISVNfVE9fU0VUIjpmYWxzZSwicG93ZXJfc2F2ZV9tb2RlX2luZm8uUkVNT1ZFX1RISVNfVE9fU0VUIjpmYWxzZSwibmV0d29ya19jb25uZWN0aXZpdHlfaW5mby5SRU1PVkVfVEhJU19UT19TRVQiOmZhbHNlLCJhbnJfaW5mby5SRU1PVkVfVEhJU19UT19TRVQiOmZhbHNlfSwiYW5yIjp7ImNhcHR1cmVfZ29vZ2xlLlJFTU9WRV9USElTX1RPX1NFVCI6dHJ1ZX0sImJhY2tncm91bmRfYWN0aXZpdHkiOnsiY2FwdHVyZV9lbmFibGVkLlJFTU9WRV9USElTX1RPX1NFVCI6dHJ1ZSwibWFudWFsX2JhY2tncm91bmRfYWN0aXZpdHlfbGltaXQuUkVNT1ZFX1RISVNfVE9fU0VUIjo1MCwibWluX2JhY2tncm91bmRfYWN0aXZpdHlfZHVyYXRpb24uUkVNT1ZFX1RISVNfVE9fU0VUIjozNTAwLCJtYXhfY2FjaGVkX2FjdGl2aXRpZXMuUkVNT1ZFX1RISVNfVE9fU0VUIjo1fX0= + \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/compressed_mapping_file.txt b/embrace-gradle-plugin/src/test/resources/compressed_mapping_file.txt new file mode 100644 index 0000000000..0af80faca6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/compressed_mapping_file.txt @@ -0,0 +1 @@ +compressed file \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/config_file_empty.json b/embrace-gradle-plugin/src/test/resources/config_file_empty.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/config_file_empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/config_file_expected.json b/embrace-gradle-plugin/src/test/resources/config_file_expected.json new file mode 100644 index 0000000000..149d87ca97 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/config_file_expected.json @@ -0,0 +1,40 @@ +{ + "app_id": "sazwW", + "api_token": "13f327e891ad45858949004eb755b9f1", + "ndk_enabled": true, + "sdk_config": { + "anr": { + "capture_google": true, + "new_attribute": "false" + }, + "newConfig": { + "test_config": 5, + "test_config_2": "false", + "test_config_3": true + }, + "crash_handler": { + "enabled": true + }, + "base_urls": { + "config.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:23002", + "data.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24000", + "data_dev.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24001", + "images.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24000" + }, + "networking": { + "capture_request_content_length": false + }, + "session": { + "async_end": true, + "max_session_seconds": 60 + }, + "startup_moment": { + "automatically_end.REMOVE_THIS_TO_SET": "123", + "automatically_end": false, + "take_screenshot": false + }, + "taps": { + "capture_coordinates": false + } + } +} \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/config_file_invalid.json b/embrace-gradle-plugin/src/test/resources/config_file_invalid.json new file mode 100644 index 0000000000..332eb4847a --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/config_file_invalid.json @@ -0,0 +1,4 @@ +{ + "app_id": "sazwW", + "wrong-key!": "value" +} \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/config_file_wrong_sdk_config.json b/embrace-gradle-plugin/src/test/resources/config_file_wrong_sdk_config.json new file mode 100644 index 0000000000..c6a506cf62 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/config_file_wrong_sdk_config.json @@ -0,0 +1,40 @@ +{ + "app_id": "sazwW", + "api_token": "13f327e891ad45858949004eb755b9f1", + "ndk_enabled": true, + "sdk_config": { + "anr": { + "capture_google": "text", + "new_attribute": "text" + }, + "newConfig": { + "test_config": "text", + "test_config_2": "text", + "test_config_3": "text" + }, + "crash_handler": { + "enabled": "text" + }, + "base_urls": { + "config.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:23002", + "data.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24000", + "data_dev.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24001", + "images.REMOVE_THIS_TO_SET": "http://YOUR_LOCAL_IP_HERE:24000" + }, + "networking": { + "capture_request_content_length": "text" + }, + "session": { + "async_end": "text", + "max_session_seconds": "text" + }, + "startup_moment": { + "automatically_end.REMOVE_THIS_TO_SET": "text", + "automatically_end": "text", + "take_screenshot": "text" + }, + "taps": { + "capture_coordinates": "text" + } + } +} \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/injected_resources.xml b/embrace-gradle-plugin/src/test/resources/injected_resources.xml new file mode 100644 index 0000000000..ad862f2227 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/injected_resources.xml @@ -0,0 +1,10 @@ + + + + appId + flavorName + buildId + buildType + true + c2RrQ29uZmln + diff --git a/embrace-gradle-plugin/src/test/resources/injected_resources_no_optional.xml b/embrace-gradle-plugin/src/test/resources/injected_resources_no_optional.xml new file mode 100644 index 0000000000..1f4145ae60 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/injected_resources_no_optional.xml @@ -0,0 +1,7 @@ + + + + buildId + buildType + true + diff --git a/embrace-gradle-plugin/src/test/resources/injected_symbol_resources b/embrace-gradle-plugin/src/test/resources/injected_symbol_resources new file mode 100644 index 0000000000..40ecef8f19 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/injected_symbol_resources @@ -0,0 +1,5 @@ + + + + eyJzeW1ib2xzIjp7ImFybWVhYmktdjhhIjp7ImxpYm15Z2FtZS5zbyI6IkYwOTgwQUNCMTk4MjMifX19 + diff --git a/embrace-gradle-plugin/src/test/resources/mapping.txt b/embrace-gradle-plugin/src/test/resources/mapping.txt new file mode 100644 index 0000000000..23a2ef8503 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/mapping.txt @@ -0,0 +1 @@ +mapping \ No newline at end of file diff --git a/embrace-gradle-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/embrace-gradle-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/embrace-gradle-plugin/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/embrace-lint/build.gradle.kts b/embrace-lint/build.gradle.kts index 08652edf49..b934b57a86 100644 --- a/embrace-lint/build.gradle.kts +++ b/embrace-lint/build.gradle.kts @@ -7,7 +7,7 @@ plugins { embrace { productionModule.set(false) - androidLibrary.set(false) + androidModule.set(false) jvmTarget.set(JavaVersion.VERSION_11) } diff --git a/embrace-test-common/build.gradle.kts b/embrace-test-common/build.gradle.kts index d5fb257563..faedaf97aa 100644 --- a/embrace-test-common/build.gradle.kts +++ b/embrace-test-common/build.gradle.kts @@ -1,13 +1,16 @@ plugins { - id("com.android.library") - id("kotlin-android") + kotlin("jvm") + alias(libs.plugins.google.ksp) id("io.embrace.internal.build-logic") } embrace { productionModule.set(false) + androidModule.set(false) } -android { - namespace = "io.embrace.android.embracesdk.test.common" +dependencies { + implementation(libs.mockwebserver) + implementation(libs.moshi) + ksp(libs.moshi.kotlin.codegen) } diff --git a/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/JdkEnv.kt b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/JdkEnv.kt new file mode 100644 index 0000000000..76a1e7ebbc --- /dev/null +++ b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/JdkEnv.kt @@ -0,0 +1,15 @@ +package io.embrace.android.gradle.config + +enum class JdkEnv(val path: String) { + JAVA_11(resolveJdkPath(11)), + JAVA_17(resolveJdkPath(17)) +} + +private fun resolveJdkPath(version: Int): String { + return System.getenv("JAVA_HOME_${version}_X64") + ?: System.getenv("LOCAL_JAVA_${version}_PATH") + ?: error( + "No JDK path supplied for Java $version. Please check the " + + "LOCAL_JAVA_${version}_PATH or JAVA_HOME_${version}_X64 envars are set." + ) +} diff --git a/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/TestMatrix.kt b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/TestMatrix.kt new file mode 100644 index 0000000000..8e97d2522e --- /dev/null +++ b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/config/TestMatrix.kt @@ -0,0 +1,47 @@ +package io.embrace.android.gradle.config + +/** + * Defines the test matrix we use in one place to ensure that test cases use sane version + * combinations. Our strategy is to pick ~5 version combinations that are representative of our + * supported range of versions. The theory is that most customers will fall somewhere along the + * middle of the version range distribution. + * + * These versions should be regularly reviewed & updated. The AGP/Gradle compatibility matrix may + * be helpful: https://developer.android.com/build/releases/gradle-plugin#updating-gradle + * + * If a specific version is required then we can add it, but we should strive to reduce special + * cases wherever possible. + */ +sealed class TestMatrix( + val agp: String, + val gradle: String, + val kotlin: String, + val jdk: JdkEnv, +) { + + /** + * The minimum version we support & run tests against. + */ + object MinVersion : TestMatrix("7.4.2", "7.5.1", "1.8.22", JdkEnv.JAVA_11) + + /** + * Older than middle of the pack, but not as bad as our minimum. + */ + object OlderVersion : TestMatrix("8.1.4", "8.1.1", "1.8.22", JdkEnv.JAVA_17) + + /** + * Middle of the pack. + */ + object MiddleVersion : TestMatrix("8.3.2", "8.4", "1.9.22", JdkEnv.JAVA_17) + + /** + * Not the latest, but newer than the middle of the pack. + */ + object NewerVersion : TestMatrix("8.5.2", "8.7", "2.0.0", JdkEnv.JAVA_17) + + /** + * The maximum version we currently run tests against. Newer versions may work, but are not + * explicitly tested. + */ + object MaxVersion : TestMatrix("8.8.0", "8.12.1", "2.1.10", JdkEnv.JAVA_17) +} diff --git a/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/MultipartFormReader.kt b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/MultipartFormReader.kt new file mode 100644 index 0000000000..4f40fa1862 --- /dev/null +++ b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/MultipartFormReader.kt @@ -0,0 +1,68 @@ +package io.embrace.android.gradle.network + +import okhttp3.MultipartReader +import okhttp3.mockwebserver.RecordedRequest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class FormPart( + val contentDisposition: String, + val data: String?, + val contentType: String? = null, +) + +class MultipartFormReader { + + fun read(request: RecordedRequest): List { + val boundary = checkNotNull(request.headers["Content-Type"]).substringAfter("boundary=") + val reader = MultipartReader(request.body, boundary) + val parts = mutableListOf() + + reader.use { + while (true) { + val part = it.nextPart() ?: break + parts.add( + FormPart( + contentDisposition = part.headers["Content-Disposition"] + ?: error("Missing Content-Disposition"), + data = part.body.readUtf8(), + contentType = part.headers["Content-Type"] + ) + ) + } + } + return parts + } +} + +fun FormPart.validateBodyAppId(expectedAppId: String) { + assertEquals("form-data; name=\"app\"", contentDisposition) + assertEquals(expectedAppId, data) +} + +fun FormPart.validateBodyApiToken(expectedApiToken: String) { + assertEquals("form-data; name=\"token\"", contentDisposition) + assertEquals(expectedApiToken, data) +} + +fun FormPart.validateBodyVariant(expectedVariantName: String) { + assertEquals("form-data; name=\"variant\"", contentDisposition) + assertEquals(expectedVariantName, data) +} + +fun FormPart.validateBodyBuildId() { + assertEquals("form-data; name=\"id\"", contentDisposition) + assertTrue(data?.length == BUILD_ID_LENGTH) +} + +fun FormPart.validateMappingFile(expectedFileName: String) { + assertEquals( + "form-data; name=\"file\"; filename=\"${expectedFileName}\"", + contentDisposition + ) + assertEquals("text/plain", contentType) + assertTrue(checkNotNull(data?.length) > 0) +} + +private const val BUILD_ID_LENGTH = 32 +const val HEADER_APP_ID = "X-EM-AID" diff --git a/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/NdkHandshakeRequestBody.kt b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/NdkHandshakeRequestBody.kt new file mode 100644 index 0000000000..903b473616 --- /dev/null +++ b/embrace-test-common/src/main/kotlin/io/embrace/android/gradle/network/NdkHandshakeRequestBody.kt @@ -0,0 +1,11 @@ +package io.embrace.android.gradle.network + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NdkHandshakeRequestBody( + val app: String, + val token: String, + val variant: String, + val archs: Map> +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fe0dacf2c..3bfed21af5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,16 @@ androidxJunit = "1.2.1" mockwebserver = "4.12.0" ksp = "2.1.0-1.0.29" nexus = "1.3.0" +appcompat = "1.7.0" +asmUtil = "9.7" +gradleTestKit = "8.11.1" +kotlin = "2.1.0" +zstdJni = "1.5.6-9" +detekt = "1.23.7" +buildconfig = "5.5.1" +agp-api = "7.4.2" +apktoolLib = "2.11.0" +bundletool = "1.18.0" [libraries] binary-compatibility-validator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } @@ -67,9 +77,19 @@ androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.re mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinExposed" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detektGradlePlugin" } +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asmUtil" } +gradle-test-kit = { module = "dev.gradleplugins:gradle-test-kit", version.ref = "gradleTestKit" } +zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstdJni" } +agp-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp-api" } +apktool-lib = { module = "org.apktool:apktool-lib", version.ref = "apktoolLib" } +bundletool = { module = "com.android.tools.build:bundletool", version.ref = "bundletool" } [plugins] google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus" } agp-library = { id = "com.android.library", version.ref = "agp" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7bdb2375c9..c0f751c114 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,10 @@ include( ":embrace-android-compose", ":embrace-lint", ":embrace-test-common", - ":embrace-test-fakes" + ":embrace-test-fakes", + ":embrace-gradle-plugin", + ":embrace-bytecode-instrumentation-tests", + ":embrace-gradle-plugin-integration-tests", ) pluginManagement {